Skip to content

Middleware Pipeline

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.

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 categories
  • filter() → added only to getList() category

Middleware executes in two phases within ApiOperationMixin:

Methods that take (HTTPServerRequest, HTTPServerResponse):

@any
void 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

Methods that take (IQuery, ...) and return IQuery:

@getList
IQuery 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

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

Crate recognizes several middleware method signatures:

@any
void handler(HTTPServerRequest req, HTTPServerResponse res) @safe { }
@getList
IQuery filter(IQuery selector, HTTPServerRequest request) @safe { }
struct QueryParams {
string status;
int page;
}
@getList
IQuery filter(IQuery selector, QueryParams params) @safe { }

Crate auto-parses query string parameters into the QueryParams struct.

@getItem
IQuery filter(IQuery selector, QueryParams params, HTTPServerRequest req) @safe { }
@mapper
Json 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.

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 matters. Given:

crateRouter.prepare(crate)
.and(authMiddleware) // 1st
.and(adminCheck) // 2nd
.and(visibilityFilter) // 3rd
.and(paginationFilter) // 4th
.and(responseMapper); // 5th

Execution order:

  1. authMiddleware — Check authentication (non-query, can 401)
  2. adminCheck — Check admin status (non-query, can 403)
  3. visibilityFilter — Filter query by visibility (query middleware)
  4. paginationFilter — Apply skip/limit (query middleware)
  5. responseMapper — Transform response items (mapper, runs during serialization)

When using multiple policies, .and() is a free function that forwards to each router:

// From crate.http.routing.multiPolicy
BlankRouter[] 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);