Actions
Execute request-aware business logic in JavaScript, TypeScript (Beta), or Rust, compiled and embedded into TitanPl’s native server.
What is an Action?
An action is a function that executes when a route is matched.
Actions receive the full request context and are responsible for all dynamic behavior in a TitanPl application.
Routes define what endpoint exists.
Actions define what happens when that endpoint is called.
TitanPl is unique because it allows you to write endpoints in JavaScript, TypeScript (Beta), and Rust within the same project.
🔵 TypeScript Actions (.ts)
BETA
Fully typed, strict, and auto-compiled. TitanPl enforces zero type errors before running.
import { defineAction } from "../../titan/titan";
interface HelloResponse {
message: string;
user_name: string;
}
// "defineAction" provides automatic type inference for "req"
export const hello = defineAction((req): HelloResponse => {
t.log("Handling request with strict types...");
return {
message: "Hello from TypeScript!",
user_name: req.body.name || "Guest"
};
});Strict Type Safety
If titan dev detects a type error, the server will not run. This ensures you never ship or test broken code.
🟡 JavaScript Actions (.js)
STANDARD
Perfect for business logic, rapid prototyping, and IO-bound tasks.
export function run(req) {
t.log("Handling user request...");
return {
message: "Hello from JavaScript!",
user_id: req.params.id
};
}🔴 Rust Actions (.rs)
BETA
Perfect for heavy computation, encryption, image processing, or low-level system access.
Note: The Native Rust Action API is currently in Beta.
use axum::{response::{IntoResponse, Json}, http::Request, body::Body};
use serde_json::json;
pub async fn run(req: Request<Body>) -> impl IntoResponse {
// Perform heavy computation here
t.log("Processed 1M records in Rust");
Json(json!({ "result": "Processed 1M records in Rust" }))
}⚡ Hybrid Action System
TitanPl automatically detects, compiles, and routes all types.
.tsfiles are type-checked and compiled..jsfiles are bundled..rsfiles are compiled into the native binary.- All share the same
routes.jsonconfiguration.
| Mode | Status | Description |
|---|---|---|
| JavaScript | ✅ Stable | Standard JS development. |
| TypeScript | ⚠️ Beta | Strict typing usage. |
| Rust + JS | ⚠️ Beta | Hybrid runtime with Rust actions. |
| Rust + TS | ⚠️ Beta | Hybrid runtime with Rust and TS actions. |
⚙️ Enabling Rust Actions
To enable Rust support in an existing Titan project:
-
Update your CLI to the latest version:
npm install -g @ezetgalaxy/titan -
Update
package.jsonto include the rust template:{ "titan": { "template": "rust" } } -
Run the update command to generate the necessary native files:
titan update
When to use an Action
Use an action whenever you need:
- Access to request data (
params,query,body,headers) - Conditional logic or validation
- Database or external service calls
- Dynamic response generation
Static responses should use reply() instead.
Connecting a Route to an Action
Routes reference actions by name.
t.get("/user/:id<number>").action("getUser")Titan automatically resolves this to:
app/actions/getUser.tsapp/actions/getUser.jsapp/actions/getUser.rs
No imports or wiring are required.
Naming Convention Warning
You MUST ensure the following match exactly:
- The file name in
app/actions(e.g.,getUser.ts) - The route reference in
app.js(e.g.,.action("getUser"))
Titan uses this convention to automatically wire your application. If they do not match, the action will not be found.
Action file structure
Actions live in the app/actions directory.
Each file exports a single function. In JavaScript/TypeScript, it's the run or default export. In Rust, it's the run function.
Accessing route parameters (req.params)
Typed route parameters are validated before the action runs.
export function run(req: Context) {
return {
userId: req.params.id,
}
}req.params.idis guaranteed to match the declared type- Invalid parameters never reach the action
Reading query parameters (req.query)
Query strings are parsed automatically.
export function run(req) {
return {
q: req.query.q,
}
}Request:
GET /search?q=titan
Result:
{ "q": "titan" }
Reading request bodies (req.body)
Titan parses request bodies based on content type.
export function run(req: Context) {
const { email } = req.body as { email: string };
return { email };
}- JSON bodies are parsed automatically
- Invalid payloads are rejected before execution
Returning responses
Actions return plain JavaScript objects (JS/TS) or IntoResponse (Rust).
export function run(req) {
return {
success: true,
}
}Titan automatically:
- Serializes objects to JSON
- Sets appropriate response headers
- Sends the response from native Rust
Returning HTTP errors
Throwing an error signals a failed request.
export function protectedAction(req) {
if (!req.headers.authorization) {
throw new Error("Unauthorized")
}
return { ok: true }
}Titan converts errors into proper HTTP responses or log on the terminal.
Powered by multi-threaded Gravity (JS)
JavaScript actions execute synchronously within the Gravity runtime, a high-performance multi-threaded engine embedded into the native Rust server.
- Multi-threaded Execution: Scaling across all CPU cores.
- No Node.js Runtime: Zero overhead from traditional event loops.
- Predictable Execution: Linear, easy-to-reason logic via Drift.
This ensures predictable execution and easy reasoning about behavior.
Separation of responsibilities
| Layer | Responsibility |
|---|---|
| Routes | URL structure and HTTP methods |
| Actions | Request handling and logic |
| Rust server | Execution, routing, and performance |
This separation keeps applications clean, testable, and maintainable.
Mental model
Routes describe structure Actions handle behavior Titan compiles both into Rust
This model scales cleanly from small APIs to large production systems.