Middleware Pipeline
Query filters (@any, @get, @create, etc.) and request middleware that run before the operation accesses data.
Crate is organized into six layers, each with a single responsibility. A request flows top-down through these layers.
Public API (crate.http.router)
crateSetup!RestApi, crateRouter.add(), crateRouter.prepare() — the functions you call in your application code. These are convenience wrappers that create and configure the layers below.
CrateRouter (crate.http.routing.crateRouter)
Multi-policy orchestration and HTTP dispatch. Integrates with vibe.d’s URLRouter, manages an array of ITypeRouter instances, and routes incoming requests to the right operation. One CrateRouter per application, parameterized by policies.
TypeRouter (crate.http.routing.typeRouter)
One instance per policy-model combination. Auto-generates CRUD operations by inspecting the Policy struct at compile time. Contains a BlankRouter internally via composition.
BlankRouter (crate.http.routing.blankRouter)
Stores an array of IApiOperation and matches requests by method, path, and optional body. No model awareness — just a flat operation registry with a match() method.
IApiOperation (crate.http.operations.*)
One per endpoint. GetListApiOperation, CreateItemApiOperation, etc. Each operation knows its rule (method, path, response format) and orchestrates: middleware pipeline, data access, response mappers, and serialization.
Data and Middleware
Middleware Pipeline
Query filters (@any, @get, @create, etc.) and request middleware that run before the operation accesses data.
Response Mappers
@mapper functions that transform each item before serialization. Used to add computed properties or hide fields. Mappers are per-type, so they apply uniformly regardless of which request triggered the response.
Crate!T (Data Access)
MemoryCrate, MongoCrate — the storage backend. Operations read/write through the Crate!T interface.
Serializer
Converts the mapped items into the wire format. RestApiSerializer wraps in model-name keys, JsonApiSerializer uses JSON:API envelopes, etc. Runs after response mappers.
The entry point. Created via router.crateSetup!RestApi. Responsibilities:
URLRouter via a catch-all any("*") handlerITypeRouter instances (TypeRouters and BlankRouters)CrateRouter!(RestApi, Mcp))onRouterInit, onRouterReady)Created by CrateRouter for each model+policy combination. When constructed, it:
CrateRuleCrateResource)TypeRouter is also responsible for exposing action methods (.expose!) and item operations (.itemOperation!).
The simplest router. An array of IApiOperation instances with a match() method. Has no model awareness — it just stores operations and finds which one matches a given HTTP method + path + optional body.
TypeRouter wraps one. CrateRouter also creates standalone BlankRouters for custom operations (via .blank()).
Interface for all endpoint handlers. Each operation knows its CrateRule (method, path, response format) and implements handle(OperationContext).
Concrete implementations use two mixin templates for shared behavior:
TypeRouter contains a BlankRouter rather than extending it. This is intentional:
ITypeRouter, so CrateRouter treats them uniformlyPolicies (RestApi, Mcp, GraphQL) are stateless structs with only static methods. This design:
describe!PolicyCrateRouter!(RestApi, Mcp))When a model references another model (e.g., Campaign has a Team field), Crate needs to resolve the Team ID to a full Team object during POST/PATCH operations. This is done through crateGetters, a global CrateAccess.GetItem[string] map:
crateGetters["Team"] = &teamCrate.getItem;Why global? Because D’s template system makes it impractical to thread type information through all the layers. A Campaign’s serializer needs to know about Team, but Campaign and Team are compiled separately. The global registry solves this at runtime.
The collectRequiredGetters!T template validates at startup that all required getters are registered.