Middleware

Intercept and process requests before they reach the route handlers.

Middleware can be used to intercept and process requests before they reach the route handlers defined as part of a Fragment. This can be used to implement features such as authentication, rate limiting, and selectively disable routes.

Middleware is fully type-safe and can be used to access the request body, path parameters, query parameters, etc.

Basic Usage

Middleware is defined as part of the Fragment's server configuration. It is called for every request and can modify the request before it reaches the route handlers. Fragno only supports a single middleware method; it is not possible to add multiple middleware handlers like in other frameworks.

Returning undefined (or void) will continue the request to the route handler. Returning a Response will short-circuit the request and return the Response. The first argument of the withMiddleware callback handler contains the input context, which can be used to access the request body, path parameters, query parameters, and other request data. The second argument contains the output context, which can be used to create responses.

lib/example-fragment-server.ts
import { createExampleFragment } from "@fragno-dev/example-fragment";

export function createExampleFragmentInstance() {
  return createExampleFragment({
    // Fragment-specific config fields
    someApiKey: process.env.EXAMPLE_API_KEY!,
  }).withMiddleware(async ({ queryParams, path, method }, { error, json }) => {
    const q = queryParams.get("q");

    if (q === "secret") {
      return undefined;
    }

    return error({ message: "Unauthorized", code: "UNAUTHORIZED" }, 401);
  });
}

Application Request Context

Direct fragment handlers can receive application-owned context without serializing it into HTTP headers. Declare the context type with withRequestContext<T>(); the resulting fragment handler and middleware retain that type through the complete request chain.

lib/example-fragment-server.ts
type ExampleRequestContext = {
  userId: string;
};

const fragment = instantiate(exampleFragmentDefinition)
  .withConfig({})
  .withRoutes(exampleRoutes)
  .withOptions({ mountRoute: "/api/example" })
  .withRequestContext<ExampleRequestContext>()
  .build()
  .withMiddleware(async ({ requestContext }, { error }) => {
    if (!requestContext) {
      return error({ message: "Unauthorized", code: "UNAUTHORIZED" }, 401);
    }

    // requestContext is ExampleRequestContext here.
    console.log(requestContext.userId);
  });

await fragment.handler(request, {
  requestContext: { userId: "user-1" } satisfies ExampleRequestContext,
});

Request context is separate from propagationContext, which carries telemetry-only W3C tracing metadata.

Restricting database reads

Middleware for a database Fragment can add request-scoped predicates through deps.queryPolicies.addRead(). Fragno automatically applies these predicates to direct reads, counts, cursor queries, and matching tables inside joins. Using the authenticated request context from the previous section:

lib/documents-fragment-server.ts
const documents = createDocumentsFragment({}, { databaseAdapter }).withMiddleware(
  ({ requestContext }, { deps, error }) => {
    if (!requestContext) {
      return error({ message: "Unauthorized", code: "UNAUTHORIZED" }, 401);
    }

    deps.queryPolicies.addRead("documents", (eb) => eb("ownerId", "=", requestContext.userId));
  },
);

The ownerId column must have a compatible index before the policy can use it. Unindexed policy predicates are rejected by TypeScript and validated again at runtime.

Query policies affect reads only and are not fail closed: if middleware adds no policy, reads remain unrestricted. See Query Policies for composition rules, coverage, and security boundaries.

Route-specific Usage

ifMatchesRoute can be used to execute middleware only for specific routes. It has full type safety for parameters and request input. The callback passed to ifMatchesRoute is only executed when the given route is matched.

Returning the Response

Make sure that the response returned from the ifMatchesRoute callback is also returned from the withMiddleware callback.

lib/example-fragment-server.ts
import { createExampleFragment } from "@fragno-dev/example-fragment";
import { logger } from "@/lib/logger";

export function createExampleFragmentInstance() {
  return createExampleFragment({}).withMiddleware(async ({ ifMatchesRoute }) => {
    const createResponse = await ifMatchesRoute("POST", "/users", async ({ input }) => {
      const body = await input.valid();
      logger.log(`Creating user with ID: ${body.id}`);
    });

    const deleteResponse = await ifMatchesRoute("DELETE", "/users/:id", async () => {
      return error(
        {
          message: "Deleting users has been disabled.",
          code: "DELETE_USERS_DISABLED",
        },
        403,
      );
    });

    if (deleteResponse) {
      return deleteResponse;
    }
  });
}

Modifying Requests

Middleware can modify requests before they reach route handlers, including query parameters, path parameters, request body, and headers:

lib/example-fragment-server.ts
export function createExampleFragmentInstance() {
  return createExampleFragment({}).withMiddleware(async ({ ifMatchesRoute, requestState }) => {
    await ifMatchesRoute("POST", "/users/:id", async ({ query, pathParams, input, headers }) => {
      // Modify query parameters
      query.set("role", "admin");

      // Modify path parameters
      pathParams.id = pathParams.id.toLowerCase();

      // Modify headers
      headers.set("X-Custom-Header", "middleware-value");

      // Modify request body
      const body = await input.valid();
      requestState.setBody({
        ...body,
        createdBy: "system",
        timestamp: Date.now(),
      });
    });
  });
}