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.
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.
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:
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.
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:
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(),
});
});
});
}