User manual

Middleware

Place a _.middleware.ts file in any routes/ subdirectory to intercept requests and responses for that subtree. Middleware applies from the root down — a _.middleware.ts at the root runs for every request.

The $ argument contains typed request data and the nearest context, as in a route handler. In this example, implement isAuthorized(apiKey) on that context; Counterfact supplies the parsed $.auth credentials. respondTo($) is the separate callback that continues request handling. See function arguments.

// routes/_.middleware.ts
import type { Middleware } from "../types/index.js";

export const middleware: Middleware["/"] = async ($, respondTo) => {
  if (!$.context.isAuthorized($.auth?.apiKey)) {
    return $.response.status(401).json({ error: "Unauthorized" });
  }

  return respondTo($);
};

respondTo($) passes the request to the next middleware layer or the route handler, and returns the response. You can modify $ before calling respondTo, modify the response after, or both.

Import the Middleware map from the generated types/index.js entry point. Select the route-directory path: Middleware["/"] for root middleware or Middleware["/admin"] for routes/admin/_.middleware.ts. Each entry uses the nearest context for that directory, including an inherited parent context. Keys are relative to the API group and do not include a configured URL prefix. Creating or removing route directories refreshes the map.

Direct imports from the mirrored _.middleware.ts type files are deprecated; existing imports remain compatible.

Response headers, logging, and scoped middleware

The examples below use the generated type for the middleware directory. Declare the response statuses, bodies, and headers your contract expects.

Adding response headers

Attach headers to every response in a subtree — useful for simulating CORS, rate-limit headers, or tracing identifiers:

// routes/_.middleware.ts
import { randomUUID } from "node:crypto";
import type { Middleware } from "../types/index.js";

export const middleware: Middleware["/"] = async ($, respondTo) => {
  const response = await respondTo($);
  return {
    ...response,
    headers: {
      ...("headers" in response ? response.headers : {}),
      "x-ratelimit-limit": "1000",
      "x-ratelimit-remaining": "999",
      "x-request-id": randomUUID(),
    },
  };
};

Request logging

Log each matched route and its response status without touching any handler:

// routes/_.middleware.ts
import type { Middleware } from "../types/index.js";

export const middleware: Middleware["/"] = async ($, respondTo) => {
  const start = Date.now();
  const response = await respondTo($);
  const elapsed = Date.now() - start;
  const status = "status" in response ? response.status : undefined;
  console.log(`${$.matchedPath} → ${status ?? "default"} (${elapsed}ms)`);
  return response;
};

Scoped middleware

Apply middleware only to a specific domain. The _.middleware.ts under routes/admin/ fires for admin routes only; all other routes are unaffected:

// routes/admin/_.middleware.ts
import type { Middleware } from "../../types/index.js";

export const middleware: Middleware["/admin"] = async ($, respondTo) => {
  const token = $.headers["x-admin-token"];
  if (token !== "super-secret") {
    return $.response.status(403).text("Forbidden");
  }
  return respondTo($);
};

See also