Routing & Middleware
Section titled “Routing & Middleware”Create files, get HTTP endpoints. Hoody Exec uses file-based routing — the filesystem IS your routing configuration. No route definitions, no Express router, no configuration files. Create a file at api/users/[id].ts and you instantly have a dynamic endpoint at /api/users/123.
File-Based Routing
Section titled “File-Based Routing”Create files → instant HTTP endpoints with Next.js-style patterns:
scripts/1/api/hello.ts → GET /api/helloscripts/1/users.ts → GET /usersscripts/1/api/users/[id].ts → GET /api/users/123scripts/1/docs/[...slug].ts → GET /docs/api/guide/introThat’s it. No route registration. No middleware configuration. The file path IS the route.
Instance Directories
Section titled “Instance Directories”Scripts are organized into instance directories. Each instance gets its own hostname and isolated script namespace.
File Storage (instance directory):
/hoody/storage/hoody-exec/scripts/1/api/users/[id].ts └┬┘ InstanceURL Paths (no instance prefix):
GET /api/users/123 → executes scripts/1/api/users/[id].tsGET /users → executes scripts/1/users.tsGET /docs/api/guide → executes scripts/1/docs/[...slug].tsThe instance number (1, 2, test, etc.) is:
- In the hostname:
exec-1,exec-2,exec-test - In the storage path:
scripts/1/,scripts/2/,scripts/test/ - NOT in the URL path:
/api/users(not/1/api/users)
Access URLs:
https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.icu/api/hellohttps://PROJECT_ID-CONTAINER_ID-exec-2.SERVER.containers.hoody.icu/users/123https://PROJECT_ID-CONTAINER_ID-exec-test.SERVER.containers.hoody.icu/docs/api/guideDynamic Route Patterns
Section titled “Dynamic Route Patterns”Hoody Exec supports Next.js-style dynamic routing with brackets:
Single Parameter — [param]
Section titled “Single Parameter — [param]”scripts/1/users/[id].ts → /users/123Access via metadata.parameters.id → "123"
Multiple Parameters — [param1]/[param2]
Section titled “Multiple Parameters — [param1]/[param2]”scripts/1/blog/[year]/[month].ts → /blog/2024/11Access via metadata.parameters.year → "2024", metadata.parameters.month → "11"
Catch-All — [...slug]
Section titled “Catch-All — [...slug]”Matches one or more path segments (requires at least one):
scripts/1/docs/[...slug].ts → /docs/api/guide/introAccess via metadata.parameters.slug → ["api", "guide", "intro"]
const parts = metadata.parameters.slug; // ["api", "guide", "intro"]const fullPath = parts.join('/'); // "api/guide/intro"return { section: parts[0], path: fullPath };Optional Catch-All — [[...path]]
Section titled “Optional Catch-All — [[...path]]”Matches zero or more path segments (also matches the bare prefix):
scripts/1/pages/[[...path]].ts → /pages OR /pages/about OR /pages/blog/post/1Access via metadata.parameters.path → [] or ["about"] or ["blog", "post", "1"]
const parts = metadata.parameters.path; // [] for /pages, ["about"] for /pages/aboutif (parts.length === 0) { return { page: "index" }; // Bare /pages}return { page: parts.join('/') }; // /pages/about → "about"Pattern Reference
Section titled “Pattern Reference”| Pattern | File Path | Matches | Parameters |
|---|---|---|---|
| Static | scripts/1/api/hello.ts | /api/hello | {} |
| Index | scripts/1/api/index.ts | /api | {} |
| Dynamic | scripts/1/users/[id].ts | /users/123 | { id: "123" } |
| Nested Dynamic | scripts/1/blog/[year]/[month].ts | /blog/2024/11 | { year: "2024", month: "11" } |
| Catch-All | scripts/1/docs/[...slug].ts | /docs/api/guide/intro | { slug: ["api", "guide", "intro"] } |
| Optional Catch-All | scripts/1/pages/[[...path]].ts | /pages or /pages/about | { path: [] } or { path: ["about"] } |
Dynamic Directory Segments
Section titled “Dynamic Directory Segments”Dynamic parameters can appear in directory names, not just file names. This lets you build deeply nested, RESTful route hierarchies:
scripts/1/users/[userId]/settings.ts → /users/42/settingsscripts/1/users/[userId]/posts/[postId].ts → /users/42/posts/99scripts/1/shops/[shopId]/products/[productId].ts → /shops/abc/products/xyzconst { userId, postId } = metadata.parameters;// userId = "42", postId = "99"const post = await db.getPost(userId, postId);return { post };File Extensions
Section titled “File Extensions”- Both
.ts(TypeScript) and.js(JavaScript) scripts are supported - TypeScript files are automatically transpiled — no build step needed
- URL paths can include or omit the file extension:
/api/helloand/api/hello.tsboth resolve index.ts(orindex.js) files match the directory root:api/index.tshandles/api
Route Priority
Section titled “Route Priority”When multiple files could match a URL, Hoody Exec uses this priority order:
- Exact match —
api/users.tsbeatsapi/[param].tsfor/api/users - Dynamic segments —
api/[id].tsbeatsapi/[...slug].tsfor/api/123 - Catch-all —
api/[...slug].tscatches everything else - Optional catch-all —
api/[[...path]].tsis the lowest priority fallback
Route Collision Handling
Section titled “Route Collision Handling”When multiple dynamic route files exist at the same directory level (e.g., [id].js and [slug].js), the first match based on filesystem order wins. This behavior is non-deterministic and should be avoided.
scripts/1/products/[id].ts ← These compete for /products/abcscripts/1/products/[slug].ts ← Filesystem order determines winnerMiddleware System (Worker Mode)
Section titled “Middleware System (Worker Mode)”Worker mode supports a pre/post middleware system that runs around requests matching a script in the same directory as the middleware files.
Execution order: pre.js → main script → post.js
// @mode worker
// Authentication checkif (!req.headers.authorization) { res.statusCode = 401; return { error: "Unauthorized" }; // Early exit — main script skipped}
// Validate token and add to sharedconst userId = validateToken(req.headers.authorization);shared.currentUser = { userId, timestamp: Date.now() };
// Return nothing to continue to main scriptKey behavior:
- Runs before requests matching a script in the same directory (
api/) - Return a value to short-circuit (skip main script)
- Return nothing (or
undefined) to continue to main script - Can set
sharedproperties for the main script to use
// Your regular endpoint script (same directory as pre.js/post.js)
const users = await db.getUsers();return { users };// @mode worker
// mainResult contains the main script's return value (concurrency-safe!)return { success: true, data: mainResult, user: shared.currentUser?.userId, timestamp: new Date().toISOString(), version: "1.0.0"};Key behavior:
- Runs after requests matching a script in the same directory (
api/) mainResultcontains the return value from the main script- Return a new value to replace/wrap the main script’s response
- Runs even when
pre.jsshort-circuits — perfect for logging, cleanup, response envelopes
Middleware Discovery
Section titled “Middleware Discovery”When a request matches a script, Hoody Exec looks for a pre.js and a post.js in the same directory as the matched script file (either .js or .ts — TypeScript is tried first). The middleware files apply to the routes in their own directory.
scripts/1/api/users/pre.js ← Runs before scripts in api/users/scripts/1/api/users/[id].ts ← Main script executesscripts/1/api/users/post.js ← Runs after, receives mainResultExecution order: pre.js → main script → post.js. The discovery does not walk up the directory tree — pre.js/post.js only apply to scripts in the directory that contains them. To wrap an entire instance, place the matched scripts and their pre.js/post.js in the same directory.
The mainResult variable is available in post.js, containing the return value from the main script. post.js runs even when pre.js short-circuits (returns a value) — so it can still log, clean up, or re-shape the response.
Route API Endpoints
Section titled “Route API Endpoints”Hoody Exec provides API endpoints for programmatic route management:
Resolve Route
Section titled “Resolve Route”Determine which script handles a given URL path:
# Resolve which script handles a URL pathhoody exec routes resolve --body '{"path":"/api/users/123"}'const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER});const result = await containerClient.exec.route.resolve({ path: '/api/users/123'});console.log(result.data); // { matched: true, path: "/api/users/123", hostname: "...", execId: "1", triedDirectories: [...] }curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/route/resolve" \ -H "Content-Type: application/json" \ -d '{"path": "/api/users/123"}'Discover Routes
Section titled “Discover Routes”List all available routes in an instance:
# Discover all routes in the exec instancehoody exec routes discoverconst routes = await containerClient.exec.route.discover();console.log(routes.data.routes); // [{ pattern: "/api/hello", file: "api/hello.ts" }, ...]curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/route/discover" \ -H "Content-Type: application/json"Test Routes
Section titled “Test Routes”Test multiple URL paths against the routing system in a single batch:
# Test multiple paths against routeshoody exec routes test --body '{"paths":["/api/users/123","/api/health","/nonexistent"]}'const test = await containerClient.exec.route.test({ paths: ['/api/users/123', '/api/health', '/nonexistent']});console.log(test.data);// { tested: 3, matched: 2, notMatched: 1, results: [// { path: "/api/users/123", matched: true, scriptPath: "...", parameters: { id: "123" } },// { path: "/api/health", matched: true, scriptPath: "...", parameters: {} },// { path: "/nonexistent", matched: false }// ]}curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/route/test" \ -H "Content-Type: application/json" \ -d '{"paths": ["/api/users/123", "/api/health", "/nonexistent"]}'