Middleware Pipeline
How Middleware Is Stored
Section titled “How Middleware Is Stored”When you call .and(middleware) on a TypeRouter or BlankRouter, the middleware object is wrapped in an IMiddlewareWrapper and appended to each operation’s middleware array:
// BlankRouter.and()BlankRouter and(T)(T middlewareObject) { auto wrapper = new MiddlewareWrapper!T(middlewareObject);
foreach (operation; this.operations) { operation.addMiddlewaresImpl(wrapper); }
return this;}Middleware wraps are stored per-operation, not per-router. This means middleware added with .and() only applies to operations that exist at the time of the call. Operations added later (via .withCustomOperation()) don’t automatically get existing middleware.
IMiddlewareWrapper
Section titled “IMiddlewareWrapper”The IMiddlewareWrapper interface provides UDA-categorized access to middleware methods:
interface IMiddlewareWrapper { // Methods categorized by operation type MiddlewareDelegate[] getList(); MiddlewareDelegate[] getItem(); MiddlewareDelegate[] create(); MiddlewareDelegate[] update(); MiddlewareDelegate[] patch(); MiddlewareDelegate[] replace(); MiddlewareDelegate[] delete_();
// Also used for rule updates void updateRule(ref CrateRule rule);}The MiddlewareWrapper!T template uses compile-time introspection to scan the middleware class for UDA-annotated methods:
class MyMiddleware { @any void any(HTTPServerRequest req, HTTPServerResponse res) @safe { ... }
@getList IQuery filter(IQuery selector, HTTPServerRequest req) @safe { ... }}The wrapper discovers:
any()→ added to ALL operation categoriesfilter()→ added only togetList()category
Two-Phase Execution
Section titled “Two-Phase Execution”Middleware executes in two phases within ApiOperationMixin:
Phase 1: Non-Query Middleware
Section titled “Phase 1: Non-Query Middleware”Methods that take (HTTPServerRequest, HTTPServerResponse):
@anyvoid any(HTTPServerRequest req, HTTPServerResponse res) @safe { // Authentication, authorization, request validation if (!isAuthenticated(req)) { res.statusCode = 401; res.writeBody("Unauthorized"); // Pipeline stops here — response is written }}These run first because:
- Authentication failures should short-circuit before touching the database
- Validation errors should be caught before building queries
- Request data may need preprocessing before query middleware sees it
Phase 2: Query Middleware
Section titled “Phase 2: Query Middleware”Methods that take (IQuery, ...) and return IQuery:
@getListIQuery filter(IQuery selector, HTTPServerRequest request) @safe { // Modify the database query based on request context if ("status" in request.query) { selector.where("status").equal(request.query["status"]); } return selector;}Query middleware modifies the IQuery object that will be executed against the database. They run after non-query middleware because:
- The query should only be built if the request is authorized
- User context from Phase 1 may be needed for visibility filters
Early Exit
Section titled “Early Exit”If any middleware writes to the response, subsequent middleware and the operation itself are skipped:
void handleMiddlewares(string attribute)(OperationContext operationContext) { foreach (item; this.middlewares) { mixin("auto middlewareList = item." ~ attribute ~ ";"); foreach (middleware; middlewareList) { middleware(operationContext);
// Check if response was already sent if (operationContext.response.headerWritten) return; } }}This enables patterns like:
- Return 401 Unauthorized from auth middleware
- Return 403 Forbidden from access control
- Return 412 Precondition Failed from validation
Middleware Method Signatures
Section titled “Middleware Method Signatures”Crate recognizes several middleware method signatures:
Request/Response Handler
Section titled “Request/Response Handler”@anyvoid handler(HTTPServerRequest req, HTTPServerResponse res) @safe { }Query Filter
Section titled “Query Filter”@getListIQuery filter(IQuery selector, HTTPServerRequest request) @safe { }Query Filter with Typed Parameters
Section titled “Query Filter with Typed Parameters”struct QueryParams { string status; int page;}
@getListIQuery filter(IQuery selector, QueryParams params) @safe { }Crate auto-parses query string parameters into the QueryParams struct.
Query Filter with Both
Section titled “Query Filter with Both”@getItemIQuery filter(IQuery selector, QueryParams params, HTTPServerRequest req) @safe { }Response Mapper
Section titled “Response Mapper”@mapperJson mapper(HTTPServerRequest req, const Json item) @trusted nothrow { }Mappers transform individual items in the response. They’re called per-item for both single-item and list responses.
Mapper Chaining
Section titled “Mapper Chaining”When multiple @mapper middleware are registered, they chain — each mapper receives the output of the previous one:
auto mapper(OperationContext ctx) { // Build a mapper delegate that chains all registered mappers return (Json item) { Json result = item; foreach (m; mapperMiddlewares) { result = m(ctx.request, result); } return result; };}Middleware Order
Section titled “Middleware Order”Middleware order matters. Given:
crateRouter.prepare(crate) .and(authMiddleware) // 1st .and(adminCheck) // 2nd .and(visibilityFilter) // 3rd .and(paginationFilter) // 4th .and(responseMapper); // 5thExecution order:
- authMiddleware — Check authentication (non-query, can 401)
- adminCheck — Check admin status (non-query, can 403)
- visibilityFilter — Filter query by visibility (query middleware)
- paginationFilter — Apply skip/limit (query middleware)
- responseMapper — Transform response items (mapper, runs during serialization)
UFCS Middleware for Multi-Policy
Section titled “UFCS Middleware for Multi-Policy”When using multiple policies, .and() is a free function that forwards to each router:
// From crate.http.routing.multiPolicyBlankRouter[] and(T)(BlankRouter[] routers, T middlewares) { foreach (router; routers) { router.and(middlewares); } return routers;}This ensures middleware applies to all protocol endpoints:
crateRouter.prepare(spaceCrate) // Returns BlankRouter[] for multi-policy .and(authMiddleware) // Applied to REST and MCP operations .and(visibilityFilter);