# Middleware

> Run code before and after your handlers with functions/_middleware.ts files that nest from the project root down to each route.

Middleware runs before your function handlers. A file named `_middleware.ts` in `functions/` or any subdirectory applies to every function route at and below that level, and its `(ctx, next)` signature wraps the handler so you can run code both before and after it. Typical uses: auth checks, request logging, and timing.

## Writing middleware

Define middleware with `defineMiddleware` from the SDK. Code before `await next()` runs on the way in; code after it runs once the handler (and any deeper middleware) has finished.

```typescript
// functions/_middleware.ts
// Applies to ALL function routes
import { defineMiddleware } from '@trayai/helix-sdk';

export default defineMiddleware(async (ctx, next) => {
  const start = Date.now();
  ctx.log.info(`→ ${ctx.method} ${ctx.path}`);

  await next();

  ctx.log.info(`← ${ctx.method} ${ctx.path} ${Date.now() - start}ms`);
});
```

The underscore prefix keeps the file out of routing: `_middleware.ts` never becomes an HTTP endpoint. See [functions and routing](/documentation/guides/functions-and-routing/) for the underscore rules.

## Scoping and execution order

Middleware applies at its directory level and below, and nested files run from root to leaf. For a `GET /api/v1/users` request:

```
functions/_middleware.ts           → runs first (root)
functions/api/v1/_middleware.ts    → runs second (nested)
functions/api/v1/users.get.ts      → handler runs last
```

Each level wraps the next. Calling `next()` passes control to the next middleware or the final handler; when that returns, execution continues after the `next()` call, unwinding back out to the root. Throwing before `next()` stops the chain, so the handler never runs:

```typescript
// functions/api/v1/_middleware.ts
// Applies to /api/v1/* routes only
import { defineMiddleware } from '@trayai/helix-sdk';

export default defineMiddleware(async (ctx, next) => {
  const apiKey = ctx.headers.get('x-api-key');
  if (!apiKey) throw ctx.error(401, 'API key required');

  // Attach data to ctx.state for downstream handlers
  ctx.state.apiKey = apiKey;

  await next();
});
```

## Passing data downstream with ctx.state

`ctx.state` is a plain object shared by every middleware and the handler for one request. Set values in middleware, read them anywhere downstream:

```typescript
// functions/api/v1/users.get.ts
import { defineFunction } from '@trayai/helix-sdk';

export default defineFunction(async (ctx) => {
  // Set by functions/api/v1/_middleware.ts above
  ctx.log.info('Request authenticated', { apiKey: ctx.state.apiKey });
  return { ok: true };
});
```

## What middleware can and cannot see

Middleware receives the same `ctx` as handlers, with a few practical differences.

Available:

- **Request metadata**: `ctx.method`, `ctx.path`, and `ctx.headers`.
- **Platform services**: `ctx.kv()`, `ctx.http` (including `ctx.http.authed()`), and `ctx.log`. These are the services the shipped product offers; see [the ctx object](/documentation/reference/context/) for which fields are design rather than product.
- **`ctx.state`**, shared with deeper middleware and the handler.
- **The post-handler window**: code after `await next()` runs once the handler is done, which is where timing and cleanup belong.

Not available:

- **Validated input.** Input schemas are named exports of individual function files, and validation types and guards the handler, so middleware can't rely on `ctx.input` matching any schema. Read `ctx.headers` and raw request metadata instead.
- **Non-function traffic.** Middleware applies to function routes only. Static assets and the SPA fallback are served without running it.

---

Canonical: https://helix.tray.ai/documentation/guides/middleware/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt