---
title: HTTP middleware
description: Apply path-scoped HTTP policy, understand execution order, and test short-circuit and pass-through behavior.
pagination: false
sidebar:
  icon: layers
---

Use HTTP middleware for policy that needs the request or response, such as CORS, cookies, or early rejection. Put it under `src/routes/middleware/`.
Keep business authorization that must apply to jobs, tools, and direct calls in domain functions as well.
There is no service-middleware layer or `middleware` option on `defineService`.

## Scaffold middleware

From your application's root:

```sh
bun run relkit add middleware order-policy --path /orders/\*
```

Creates `src/routes/middleware/order-policy.middleware.ts` with a pass-through `next()` call.
It does not implement authentication or authorization. `--path` is required headlessly; the
interactive prompt suggests `*`. Use a URL scope such as `/orders/:orderId` or a trailing
wildcard, not bracket-directory syntax. No service selection is needed.

For every middleware scaffolding flag, see the [middleware command reference](/docs/api/cli#relkit-add-middleware-name).

## Define path-scoped policy

```ts title="examples/commerce/src/routes/middleware/order-auth.middleware.ts"
/** Checks the demonstration order authorization function before forwarding matching HTTP requests; denial returns 401. */
import { defineMiddleware } from "@relkit/app/routes";
import orders from "../../services/orders/service.js";

const orderAuth = defineMiddleware("/orders/*", async (context, next) => {
  const result = await orders.authorizeOrder.invoke({
    authorization: context.req.header("authorization") ?? "",
  });
  if (!result.allowed) return context.json({ error: "unauthorized" }, 401);
  await next();
});

export default orderAuth;
```

The middleware reads a header, delegates the decision to a public function, and returns `401` when
denied. It calls `next()` only when processing should continue.

> **This example is not token verification**
>
> The commerce authorization function only checks for a `Bearer ` prefix. It demonstrates the
> control flow, not production authentication. Use a verified session or credential validation for
> real access control.

The handler receives the Hono `context`, `next`, and a RELKIT context containing `signal`, `env`,
`auth`, `log`, and `time`. It does not receive the route target's validated input or its dependency clients.
Call public service functions for domain behavior rather than importing their private implementation.

## Choose the match scope

| Pattern            | Intended scope         |
| ------------------ | ---------------------- |
| `*`                | Global middleware      |
| `/orders`          | The collection path    |
| `/orders/:orderId` | A named path parameter |
| `/orders/*`        | An Orders path subtree |

Middleware patterns use `:parameter` and a trailing `*`, not the route directories' bracket syntax.
The declared pattern controls matching; nesting a middleware file under a directory does not scope it.
Use the narrowest pattern that covers the policy, and test both a subtree's base path and its children.
Include callback and preflight requests in those tests so broad authentication does not block them accidentally.

## Understand execution order

For a function-backed HTTP endpoint, processing runs through framework request setup and limits,
registered authentication, matching HTTP middleware, the route's rate limiter, request mapping,
function input validation and invocation, then response mapping.

Matching declared middleware is registered in normalized ID order, not service membership or import
order. `await next()` uses onion-style execution: before-code runs inward, and after-code runs outward
in reverse order. Returning a `Response` without `next()` skips the downstream route.

Consequently, middleware can reject before mapping or read a header before the rate limiter, but it
cannot assume function input has been validated. Avoid consuming the raw body stream in middleware
when the route still needs to parse it. Use header/session policy where possible and test any body access.

## Verify short-circuit and pass-through

With the commerce app running locally in test mode on its configured port `4000`:

```sh
curl -i http://localhost:4000/orders/order-1
curl -i http://localhost:4000/orders/order-1 \
  -H 'authorization: Bearer example'
```

The first request should return the middleware's `401`. The second passes this demo policy and reaches
the normal route outcome; that may be a declared not-found error if the order does not exist.
Use [local development](/docs/learn/local-development) to start your own app and adjust its port.

Add integration assertions for a denied request, an accepted request, a path outside the scope,
and response headers after `next()`. Assert that denial prevents the target from running.
Middleware-only statuses are not automatically included in the route's OpenAPI response contract;
document them alongside [responses](/docs/modules/http/responses).

Run middleware assertions against the actual server pipeline. The current `createTestApplication`
HTTP helper does not execute declared HTTP middleware, so its success alone cannot verify this policy.

## Include alternate entry points in policy tests

Generated clients call `/rpc`, not the original REST URL. The RPC runtime replays associated
path-scoped middleware, but it sees the native RPC request, not reconstructed REST path or body values.
Read [Generated clients](/docs/modules/http/generated-clients) before relying on path/body-dependent policy.
Test both transports if both are reachable.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/routes` API](/docs/api/routes)

- [`examples/commerce/src/routes/middleware/order-auth.middleware.ts`](https://github.com/rel-kit/relkit/blob/main/examples/commerce/src/routes/middleware/order-auth.middleware.ts)
