# Custom Middleware

Write your own middleware to intercept requests, modify headers, transform responses, and handle errors.

## Writing a Middleware

`defineMiddleware(name, phase, handler)` builds one. A `before` handler receives the `MiddlewareContext`, an `after` handler the context and the response, and a `wrap` handler the context and `next()`, and returns a `Promise<MiddlewareResponse>`. The name is what Studio shows for it:

```ts title="lib/middleware/timing.ts"
import { defineMiddleware } from "@noverax/courier/middleware"

export const timingMiddleware = defineMiddleware("timing", "wrap", async (ctx, next) => {
  const start = performance.now()
  const response = await next()
  const ms = Math.round(performance.now() - start)

  response.headers.set("X-Response-Time", `${ms}ms`)
  return response
})
```

## Accessing Context

The `ctx` object gives you full visibility into the current request:

```ts title="types"
type MiddlewareContext = {
  source: string                       // Source name (e.g., "api")
  group?: string                       // Group name (e.g., "users")
  method: string                       // Endpoint method (e.g., "getAll")
  variables?: unknown                  // Request body/params — mutable
  headers: Headers                     // Request headers — mutable
  transport?: "http" | "sse" | "ws"    // What kind of call it is
  request?: Request                    // The incoming request, when a proxy runs the call
  kind?: "query" | "mutation"          // Whether the endpoint reads or writes
  carriesSession?: boolean             // Whether the call carries the user's session
  sessionPartition?: string            // Tells one user's calls from the next in a browser or an app
  signal?: AbortSignal                 // Fires when the caller stops waiting
  purpose?: "csrf-token"               // A request the session makes, not an endpoint's call
  agent?: AgentCall                    // The agent the call comes from, when an MCP server made it
}
```

A `before` handler can read or modify any of these; the call goes on with what it left:

```ts title="lib/middleware/enrich.ts"
import { defineMiddleware } from "@noverax/courier/middleware"

export const enrichMiddleware = defineMiddleware("enrich", "before", (ctx) => {
  // Add a custom header
  ctx.headers.set("X-Request-Source", ctx.source)

  // Inject a default variable
  if (ctx.variables && typeof ctx.variables === "object") {
    ctx.variables = { ...ctx.variables, _locale: "en" }
  }
})
```

## Calling next()

In a `wrap` handler, `next()` calls the next middleware in the chain, or the endpoint handler if this is the innermost middleware. The handler **must** either call `next()` and return its result, or return its own `MiddlewareResponse`.

### Pass-through

The simplest wrap handler just calls `next()` and returns the result:

```ts
import { defineMiddleware } from "@noverax/courier/middleware"

const passthrough = defineMiddleware("passthrough", "wrap", (_ctx, next) => next())
```

### Transform the response

An `after` handler receives the response and returns it, changed or not:

```ts title="lib/middleware/unwrap.ts"
import { defineMiddleware } from "@noverax/courier/middleware"

// Unwrap a { data, meta } API envelope
export const unwrapMiddleware = defineMiddleware("unwrap", "after", (_ctx, response) => {
  const body = response.data
  if (body && typeof body === "object" && "data" in body) {
    return { ...response, data: body.data }
  }
  return response
})
```

### Short-circuit (skip the endpoint)

Return a `MiddlewareResponse` without calling `next()` to prevent the request from executing:

```ts title="lib/middleware/maintenance.ts"
import { defineMiddleware } from "@noverax/courier/middleware"

export const maintenanceMiddleware = defineMiddleware("maintenance", "wrap", async (_ctx, next) => {
  if (isMaintenanceMode()) {
    return {
      status: 503,
      data: { message: "Service temporarily unavailable" },
      headers: new Headers(),
    }
  }

  return next()
})
```

An answer whose status is not a success fails the call as the upstream's own error answer would: a caller's promise rejects with an `HTTPError` carrying that status and body, whether it called directly or through the proxy, and Studio shows the call as failed. A `2xx` answer is the call's data — a cache hit, a stub.

### Retry

Call `next()` multiple times to implement retry logic:

```ts title="lib/middleware/retry-on-stale.ts"
import { defineMiddleware } from "@noverax/courier/middleware"

export const retryOnStale = defineMiddleware("retry-on-stale", "wrap", async (ctx, next) => {
  const response = await next()

  // Retry once if the response indicates stale data
  if (response.headers.get("X-Data-Stale") === "true") {
    ctx.headers.set("Cache-Control", "no-cache")
    return next()
  }

  return response
})
```

## Error Handling

Wrap `next()` in a try/catch to intercept errors from downstream middleware or the endpoint handler:

```ts title="lib/middleware/error-reporter.ts"
import { defineMiddleware } from "@noverax/courier/middleware"
import * as Sentry from "@sentry/node"

export const errorReporter = defineMiddleware("error-reporter", "wrap", async (ctx, next) => {
  try {
    return await next()
  } catch (error) {
    Sentry.captureException(error, {
      tags: {
        source: ctx.source,
        group: ctx.group,
        method: ctx.method,
      },
    })
    throw error // re-throw so the caller still receives the error
  }
})
```

> **Always re-throw or return**
>
> If you catch an error, either re-throw it or return a valid `MiddlewareResponse`. Swallowing errors silently will cause the caller to receive `undefined` instead of data, which leads to confusing bugs.

## Middleware Factories

For configurable middleware, write a factory function that returns a `Middleware`:

```ts title="lib/middleware/slow-call-warning.ts"
import { defineMiddleware } from "@noverax/courier/middleware"
import type { Middleware } from "@noverax/courier/middleware"

export function slowCallWarning(thresholdMs: number): Middleware {
  return defineMiddleware("slow-call-warning", "wrap", async (ctx, next) => {
    const start = performance.now()
    try {
      return await next()
    } finally {
      const ms = Math.round(performance.now() - start)
      if (ms > thresholdMs) console.warn(`${ctx.group}.${ctx.method} took ${ms}ms`)
    }
  })
}
```

Use it like any other middleware:

```ts title="lib/sources.ts"
import { defineApi } from "@noverax/courier/define"
import { slowCallWarning } from "./middleware/slow-call-warning"

const api = defineApi({
  baseURL: "https://api.example.com",
  middleware: [slowCallWarning(1000)],
  groups: {},
})
```

A middleware cannot cancel the request it wraps; to limit how long a call may take, use the `timeout` option.

## Composing Custom Middleware

Use `composeMiddleware` to bundle multiple middleware into a reusable unit. It returns a wrap handler, which `defineMiddleware` turns into one middleware:

```ts title="lib/middleware/production.ts"
import { composeMiddleware, defineMiddleware } from "@noverax/courier/middleware"
import { errorReporter } from "./error-reporter"
import { timingMiddleware } from "./timing"
import { unwrapMiddleware } from "./unwrap"

// Single middleware that applies all three
export const productionMiddleware = defineMiddleware(
  "production",
  "wrap",
  composeMiddleware([errorReporter, timingMiddleware, unwrapMiddleware]),
)
```

Then use the composed middleware in your source definition:

```ts title="lib/sources.ts"
import { defineApi } from "@noverax/courier/define"
import { productionMiddleware } from "./middleware/production"

const api = defineApi({
  baseURL: "https://api.example.com",
  middleware: [productionMiddleware],
  groups: {},
})
```

## Conditional Middleware

Apply middleware only for specific sources, groups, or methods by checking `ctx`:

```ts title="lib/middleware/admin-only.ts"
import { defineMiddleware } from "@noverax/courier/middleware"

export const adminGuard = defineMiddleware("admin-guard", "wrap", async (ctx, next) => {
  // Only enforce on admin endpoints
  if (ctx.group === "admin") {
    const isAdmin = await checkAdminPermission()
    if (!isAdmin) {
      return {
        status: 403,
        data: { message: "Admin access required" },
        headers: new Headers(),
      }
    }
  }

  return next()
})
```

## Next Steps

- [Built-in Middleware](/docs/middleware/built-in) -- reference for all shipped middleware
- [Middleware Overview](/docs/middleware/overview) -- the onion model, the three phases and the core types
