Skip to content
Hoody.com

Hoody Exec routes by file path: the directory tree under scripts/ is the routing configuration. There is no route table to maintain and no Express-style router to mount. Create a file at api/users/[id].ts and it serves a dynamic endpoint at /api/users/123.


Files map to URL paths using Next.js-style patterns:

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

Hoody Exec derives these mappings from the directory layout, together with any middleware placed beside the route files.


Scripts live in instance directories. Each instance has its own hostname and an isolated script namespace.

File storage (instance directory):

/hoody/storage/hoody-exec/scripts/default/1/api/users/[id].ts
└──┬──┘ └┬┘
Subdomain Instance

The default segment is the subdomain namespace. It is the literal string default for a hostname with no subdomain in front of the project ID, which covers every URL on this page. A request to myapp.PROJECT_ID-CONTAINER_ID-exec-1... resolves under scripts/myapp/1/ instead.

URL paths (no subdomain or instance prefix):

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

The instance ID (1, 2, test, and so on) appears in the hostname and in the storage path, but not in the URL:

  • Hostname: exec-1, exec-2, exec-test
  • Storage path: scripts/default/1/, scripts/default/2/, scripts/default/test/
  • URL path: /api/users, never /1/api/users

Access URLs:

https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/hello
https://PROJECT_ID-CONTAINER_ID-exec-2.SERVER.containers.hoody.com/users/123
https://PROJECT_ID-CONTAINER_ID-exec-test.SERVER.containers.hoody.com/docs/api/guide

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

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

The value arrives as metadata.parameters.id: "123".

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

The values arrive as metadata.parameters.year ("2024") and metadata.parameters.month ("11").

Matches one or more path segments:

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

The segments arrive as an array in metadata.parameters.slug: ["api", "guide", "intro"].

scripts/default/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, including the bare prefix:

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

The segments arrive as metadata.parameters.path: [], ["about"], or ["blog", "post", "1"].

scripts/default/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/default/1/api/hello.ts/api/hello{}
Indexscripts/default/1/api/index.ts/api{}
Dynamicscripts/default/1/users/[id].ts/users/123{ id: "123" }
Nested dynamicscripts/default/1/blog/[year]/[month].ts/blog/2024/11{ year: "2024", month: "11" }
Catch-allscripts/default/1/docs/[...slug].ts/docs/api/guide/intro{ slug: ["api", "guide", "intro"] }
Optional catch-allscripts/default/1/pages/[[...path]].ts/pages or /pages/about{ path: [] } or { path: ["about"] }

Dynamic parameters can appear in directory names, not only in file names. That is how you build nested REST-style resource paths:

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

  • Hoody Exec runs both .ts (TypeScript) and .js (JavaScript) scripts
  • TypeScript is transpiled automatically, so there is no build step
  • 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 match: api/users.ts beats api/[param].ts for /api/users
  2. Dynamic segments: api/[id].ts beats api/[...slug].ts for /api/123
  3. Catch-all: api/[...slug].ts catches everything else
  4. Optional catch-all: api/[[...path]].ts is the lowest priority fallback

Two dynamic route files at the same directory level ([id].ts and [slug].ts, for example) compete: the first match in filesystem order wins, and that order is not deterministic.

scripts/default/1/products/[id].ts ← These compete for /products/abc
scripts/default/1/products/[slug].ts ← Filesystem order determines winner

Hoody Exec supports pre/post middleware in both worker and serverless mode. A pre.ts and a post.ts run around any request that matches a script in the same directory as the middleware files.

Execution order: pre.ts → main script → post.ts

scripts/default/1/api/pre.ts
// @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

Behavior in pre.ts:

  • 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.ts and a post.ts in the same directory as the matched script file. Either extension works (.js or .ts), and TypeScript is tried first. The middleware files apply to the routes in their own directory.

scripts/default/1/api/users/pre.ts ← Runs before scripts in api/users/
scripts/default/1/api/users/[id].ts ← Main script executes
scripts/default/1/api/users/post.ts ← Runs after, receives mainResult

Execution order: pre.ts → main script → post.ts. Discovery does not walk up the directory tree: pre.ts and post.ts apply only to scripts in the directory that contains them. To wrap an entire instance, place the matched scripts and their pre.ts/post.ts in the same directory.

post.ts receives the main script’s return value in mainResult. It runs even when pre.ts short-circuits by returning a value, so it can still log, clean up, or reshape the response.


Hoody Exec exposes its router over HTTP 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"}' -c CONTAINER_ID -o json

List all available routes in an instance:

Terminal window
# Discover all routes in the exec instance
hoody exec routes discover -c CONTAINER_ID -o json

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"]}' -c CONTAINER_ID -o json