Skip to content

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.


Create files → instant HTTP endpoints with Next.js-style patterns:

scripts/1/api/hello.ts → GET /api/hello
scripts/1/users.ts → GET /users
scripts/1/api/users/[id].ts → GET /api/users/123
scripts/1/docs/[...slug].ts → GET /docs/api/guide/intro

That’s it. No route registration. No middleware configuration. The file path IS the route.


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
└┬┘
Instance

URL Paths (no instance prefix):

GET /api/users/123 → executes scripts/1/api/users/[id].ts
GET /users → executes scripts/1/users.ts
GET /docs/api/guide → executes scripts/1/docs/[...slug].ts

The 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/hello
https://PROJECT_ID-CONTAINER_ID-exec-2.SERVER.containers.hoody.icu/users/123
https://PROJECT_ID-CONTAINER_ID-exec-test.SERVER.containers.hoody.icu/docs/api/guide

Hoody Exec supports Next.js-style dynamic routing with brackets:

scripts/1/users/[id].ts → /users/123

Access via metadata.parameters.id"123"

scripts/1/blog/[year]/[month].ts → /blog/2024/11

Access via metadata.parameters.year"2024", metadata.parameters.month"11"

Matches one or more path segments (requires at least one):

scripts/1/docs/[...slug].ts → /docs/api/guide/intro

Access via metadata.parameters.slug["api", "guide", "intro"]

scripts/1/docs/[...slug].ts
const parts = metadata.parameters.slug; // ["api", "guide", "intro"]
const fullPath = parts.join('/'); // "api/guide/intro"
return { section: parts[0], path: fullPath };

Matches zero or more path segments (also matches the bare prefix):

scripts/1/pages/[[...path]].ts → /pages OR /pages/about OR /pages/blog/post/1

Access via metadata.parameters.path[] or ["about"] or ["blog", "post", "1"]

scripts/1/pages/[[...path]].ts
const parts = metadata.parameters.path; // [] for /pages, ["about"] for /pages/about
if (parts.length === 0) {
return { page: "index" }; // Bare /pages
}
return { page: parts.join('/') }; // /pages/about → "about"

PatternFile PathMatchesParameters
Staticscripts/1/api/hello.ts/api/hello{}
Indexscripts/1/api/index.ts/api{}
Dynamicscripts/1/users/[id].ts/users/123{ id: "123" }
Nested Dynamicscripts/1/blog/[year]/[month].ts/blog/2024/11{ year: "2024", month: "11" }
Catch-Allscripts/1/docs/[...slug].ts/docs/api/guide/intro{ slug: ["api", "guide", "intro"] }
Optional Catch-Allscripts/1/pages/[[...path]].ts/pages or /pages/about{ path: [] } or { path: ["about"] }

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/settings
scripts/1/users/[userId]/posts/[postId].ts → /users/42/posts/99
scripts/1/shops/[shopId]/products/[productId].ts → /shops/abc/products/xyz
scripts/1/users/[userId]/posts/[postId].ts
const { userId, postId } = metadata.parameters;
// userId = "42", postId = "99"
const post = await db.getPost(userId, postId);
return { post };

  • 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/hello and /api/hello.ts both resolve
  • index.ts (or index.js) files match the directory root: api/index.ts handles /api

When multiple files could match a URL, Hoody Exec uses this priority order:

  1. Exact matchapi/users.ts beats api/[param].ts for /api/users
  2. Dynamic segmentsapi/[id].ts beats api/[...slug].ts for /api/123
  3. Catch-allapi/[...slug].ts catches everything else
  4. Optional catch-allapi/[[...path]].ts is the lowest priority fallback

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/abc
scripts/1/products/[slug].ts ← Filesystem order determines winner

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

scripts/1/api/pre.js
// @mode worker
// Authentication check
if (!req.headers.authorization) {
res.statusCode = 401;
return { error: "Unauthorized" }; // Early exit — main script skipped
}
// Validate token and add to shared
const userId = validateToken(req.headers.authorization);
shared.currentUser = { userId, timestamp: Date.now() };
// Return nothing to continue to main script

Key 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 shared properties for the main script to use

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 executes
scripts/1/api/users/post.js ← Runs after, receives mainResult

Execution 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.


Hoody Exec provides API endpoints for programmatic route management:

Determine which script handles a given URL path:

Terminal window
# Resolve which script handles a URL path
hoody exec routes resolve --body '{"path":"/api/users/123"}'

List all available routes in an instance:

Terminal window
# Discover all routes in the exec instance
hoody exec routes discover

Test multiple URL paths against the routing system in a single batch:

Terminal window
# Test multiple paths against routes
hoody exec routes test --body '{"paths":["/api/users/123","/api/health","/nonexistent"]}'