# Routing & Middleware

**Page:** kit/exec/routing

[Download Raw Markdown](./kit/exec/routing.md)

---

# 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

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.

---

## 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
                                  └┬┘
                               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:**
```txt
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
```


Use multiple instances to organize different concerns: `exec-1` for your main API, `exec-2` for webhooks, `exec-test` for development. Each instance has its own scripts directory and hostname.


---

## Dynamic Route Patterns

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

### Single Parameter — `[param]`

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

Access via `metadata.parameters.id` → `"123"`

### Multiple Parameters — `[param1]/[param2]`

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

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

### Catch-All — `[...slug]`

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"]`

```javascript
// 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 };
```


Catch-all requires **at least one** segment. `/docs` alone will **not** match `docs/[...slug].ts` — use optional catch-all `[[...slug]]` if you need to match the bare prefix too.


### 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/1
```

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

```javascript
// 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"
```

---

## 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"] }` |


**Key difference:** `[...slug]` requires at least one segment after the prefix (fails on `/docs` alone), while `[[...path]]` also matches the bare prefix (`/pages` returns `{ path: [] }`).


---

## 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/settings
scripts/1/users/[userId]/posts/[postId].ts     → /users/42/posts/99
scripts/1/shops/[shopId]/products/[productId].ts → /shops/abc/products/xyz
```

```javascript
// 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 };
```

---

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

---

## Route Priority

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

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


**Best practice:** Avoid placing multiple competing dynamic segments in the same directory. If you need different parameter names, use distinct path prefixes instead:

```
scripts/1/products/by-id/[id].ts     → /products/by-id/123
scripts/1/products/by-slug/[slug].ts → /products/by-slug/cool-widget
```


---

## 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`


  

```javascript
// 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




```javascript
// scripts/1/api/users.js
// Your regular endpoint script (same directory as pre.js/post.js)

const users = await db.getUsers();
return { users };
```

  
  

```javascript
// scripts/1/api/post.js
// @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/`)
- `mainResult` contains the return value from the main script
- Return a new value to replace/wrap the main script's response
- Runs even when `pre.js` short-circuits — perfect for logging, cleanup, response envelopes





Middleware files (`pre.js`, `post.js`) are discovered in the **same directory as the matched script** — place them alongside the route files they wrap.

**Worker mode only** — middleware is not supported in serverless mode.


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


**Serverless mode limitation** — Middleware (`pre.js`/`post.js`) requires worker mode (`@mode worker`). In serverless mode, each request gets a fresh VM, so middleware state cannot be shared between the middleware and the main script. If you need request processing pipelines in serverless mode, handle pre/post logic within the script itself.


---

## Route API Endpoints

Hoody Exec provides API endpoints for programmatic route management:

### Resolve Route

Determine which script handles a given URL path:


  
    ```bash
    # Resolve which script handles a URL path
    hoody exec routes resolve --body '{"path":"/api/users/123"}'
    ```
  
  
    ```typescript
    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: [...] }
    ```
  
  
    ```bash
    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

List all available routes in an instance:


  
    ```bash
    # Discover all routes in the exec instance
    hoody exec routes discover
    ```
  
  
    ```typescript
    const routes = await containerClient.exec.route.discover();
    console.log(routes.data.routes); // [{ pattern: "/api/hello", file: "api/hello.ts" }, ...]
    ```
  
  
    ```bash
    curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/route/discover" \
      -H "Content-Type: application/json"
    ```
  


### Test Routes

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


  
    ```bash
    # Test multiple paths against routes
    hoody exec routes test --body '{"paths":["/api/users/123","/api/health","/nonexistent"]}'
    ```
  
  
    ```typescript
    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 }
    // ]}
    ```
  
  
    ```bash
    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"]}'
    ```
  


---

## What's Next