Middleware
What is Middleware?
Section titled “What is Middleware?”Middleware in Crate are classes that intercept requests at various points in the operation pipeline. They can:
- Filter queries — restrict which items are returned based on request parameters
- Validate data — reject invalid create/update requests
- Map responses — transform how items appear in the response
- Block requests — enforce authentication or authorization
Middleware classes use User-Defined Attributes (UDAs) to declare which operations they apply to.
UDA Annotations
Section titled “UDA Annotations”Each middleware method is annotated with one or more UDAs that control when it runs:
| UDA | Applies To |
|---|---|
@any | All operations |
@get | Both GET list and GET item |
@getList | GET list only |
@getItem | GET item only |
@create | POST create |
@afterCreate | POST create (post-persist hook) |
@patch | PATCH update |
@replace | PUT replace |
@put | PUT replace (alias for @replace) |
@update | Both PATCH and PUT |
@delete_ | DELETE |
@mapper | Response mapping (transforms JSON output) |
Writing a Query Filter
Section titled “Writing a Query Filter”A query filter modifies the database query based on request parameters. It receives an IQuery selector and returns a modified one:
class TypeFilter { @any IQuery any(IQuery selector, HTTPServerRequest request) @safe { if ("type" !in request.query) { return selector; }
selector.where("position.type").equal(request.query["type"]); return selector; }}Use it:
crateRouter .prepare(siteCrate) .and(new TypeFilter);Now GET /sites?type=Point returns only sites with position.type == "Point".
Query Filter with Typed Parameters
Section titled “Query Filter with Typed Parameters”Instead of manually parsing query strings, define a QueryParams struct inside your filter class:
class PaginationFilter { struct QueryParams { int page = 1; int perPage = 20; }
@getList IQuery getList(IQuery selector, QueryParams params) @safe { selector.skip((params.page - 1) * params.perPage); selector.limit(params.perPage); return selector; }}Crate automatically parses query string parameters into your QueryParams struct.
Writing a Request Middleware
Section titled “Writing a Request Middleware”Request middleware intercepts the HTTP request before the operation runs. It can modify the request, block it, or set response headers:
class AuthMiddleware { @any void any(HTTPServerRequest req, HTTPServerResponse res) @safe { if ("Authorization" !in req.headers) { res.statusCode = 401; res.writeBody("Unauthorized"); return; // Stops the pipeline } }}Operation-Specific Middleware
Section titled “Operation-Specific Middleware”Apply different logic to different operations:
class AccessControl { @getList @getItem void readAccess(HTTPServerRequest req, HTTPServerResponse res) @safe { // Public read access - no check needed }
@create @replace @patch @delete_ void writeAccess(HTTPServerRequest req, HTTPServerResponse res) @safe { if (!isAdmin(req)) { res.statusCode = 403; res.writeBody("Admin access required"); } }}Writing a Response Mapper
Section titled “Writing a Response Mapper”A mapper transforms JSON items before they’re sent in the response. Use the @mapper UDA:
class UserMapper { @mapper Json mapper(HTTPServerRequest req, const Json item, ) @trusted nothrow { try { Json result = item.clone; // Remove sensitive fields from the response result.remove("passwordHash"); result.remove("internalNotes"); return result; } catch (Exception) { return item; } }}Mappers run on each item in the response, both for single-item and list responses.
Chaining Middleware
Section titled “Chaining Middleware”Use .and() to chain multiple middleware on a route:
crateRouter .prepare(userCrate) .and(authMiddleware) .and(adminCheck) .and(validationFilter) .and(userMapper) .and(paginationFilter);Middleware runs in the order they’re added. This matters because:
- Authentication should run first (reject unauthorized requests early)
- Query filters should run before pagination
- Mappers run on the response after the operation completes
Middleware with add()
Section titled “Middleware with add()”For simpler models, pass middleware directly to add() as arguments:
crateRouter.add( productCrate, authMiddleware, validationFilter, paginationFilter);This is equivalent to calling prepare() followed by .and() for each middleware.
Post-Create Hooks
Section titled “Post-Create Hooks”The @afterCreate UDA marks middleware methods that run after an item has been persisted to the database but before the response is sent. This is useful for side effects like notifications, cache invalidation, or enriching the response:
class AuditMiddleware { @afterCreate void onCreated(HTTPServerRequest req, HTTPServerResponse res) @safe { // The item is already persisted at this point auto userId = req.context.get("userId", ""); logAuditEvent("created", userId); }}Register it like any other middleware:
crateRouter .prepare(documentCrate) .and(new AuditMiddleware);The execution order for a POST create is:
@createmiddleware runs (validation, auth)- Item is deserialized and persisted to database
@afterCreatemiddleware runs (post-persist hooks)- Response mappers apply and response is sent
Global Middleware
Section titled “Global Middleware”Use .globalMiddleware() to apply middleware to all routers and operations, including protocol-level operations like MCP’s tools/list, initialize, and discovery tools:
crateRouter .globalMiddleware(authMiddleware) .globalMiddleware(loggingMiddleware) .add(userCrate) .add(teamCrate);Unlike .and() which only applies to operations on a specific model, global middleware covers:
- All model CRUD operations across all protocols
- Protocol-level operations (MCP
tools/list,initialize, etc.) - Operations added after the middleware was registered
Global middleware returns this, enabling method chaining.
Next Steps
Section titled “Next Steps”- Create custom operations for non-CRUD endpoints
- Set up authentication with OAuth2