Policy System
What is a Policy?
Section titled “What is a Policy?”A policy is a stateless D struct with static methods that generate CrateRule objects. Each method defines one type of CRUD operation with protocol-specific routing rules — how the request arrives, how the response is formatted, and what serializer is used.
Crate ships four policies. Each one exposes the same underlying Crate!T data through a different wire protocol.
Built-In Policies
Section titled “Built-In Policies”RestApi
Section titled “RestApi”Standard JSON REST endpoints. Path-based routing, one endpoint per operation.
- Spec: Conventional JSON over HTTP
- MIME:
application/json - Serializer:
RestApiSerializer— wraps responses in a property key ({"users": [...]})
import crate.protocols.restApi.policy;
auto crateRouter = router.crateSetup!RestApi;crateRouter.add(userCrate);Generated endpoints:
| Method | Path | Operation |
|---|---|---|
GET | /users | List |
GET | /users/:id | Get item |
POST | /users | Create |
PUT | /users/:id | Replace |
PATCH | /users/:id | Update |
DELETE | /users/:id | Delete (204) |
JsonApi
Section titled “JsonApi”Follows the JSON:API specification. Same path-based routing as REST, but uses the JSON:API envelope format with data, relationships, and included sections.
- Spec: jsonapi.org
- MIME:
application/vnd.api+json - Serializer:
JsonApiSerializer— formats per JSON:API spec with relationship links
import crate.protocols.jsonApi.policy;
auto crateRouter = router.crateSetup!JsonApi;crateRouter.add(userCrate);Same six CRUD endpoints as REST, plus relationship endpoints:
| Method | Path | Operation |
|---|---|---|
GET | /users/:id/relationships/:rel | Get relationship |
PATCH | /users/:id/relationships/:rel | Update relationship |
POST | /users/:id/relationships/:rel | Add to relationship |
DELETE | /users/:id/relationships/:rel | Remove from relationship |
Clients must send Accept: application/vnd.api+json. Responses use 201 for creates (with Location header) and 204 for deletes.
Implements the Model Context Protocol over JSON-RPC 2.0. All operations go through a single POST /mcp endpoint, differentiated by the request body.
- Spec: modelcontextprotocol.io
- MIME:
application/json - Serializer:
McpSerializer— extracts arguments from JSON-RPC params, wraps responses in JSON-RPC envelope
import crate.protocols.mcp.policy;
auto crateRouter = router.crateSetup!Mcp;crateRouter.add(userCrate);All operations use POST /mcp with body-based routing:
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "list_users", "arguments": {} }, "id": 1}Tool names follow the pattern get_user, list_users, create_user, update_user, delete_user.
MCP registers additional base operations via onRouterInit:
POST /mcpwithmethod: "initialize"— protocol handshakePOST /mcpwithmethod: "tools/list"— list available toolsGET /mcp/sse— Server-Sent Events stream
GraphQL
Section titled “GraphQL”Implements GraphQL over HTTP. Single POST /graphql endpoint with query/mutation routing.
- Spec: graphql.org
- MIME:
application/json - Serializer:
GraphQLSerializer— filters response fields to match the requested selection set
import crate.protocols.graphql.policy;
auto crateRouter = router.crateSetup!GraphQL;crateRouter.add(userCrate);Queries and mutations are extracted from the GraphQL request body:
{ "query": "{ users { _id name email } }"}{ "query": "mutation { createUser(input: { name: \"Alice\" }) { _id name } }"}GraphQL registers a base operation via onRouterInit to handle schema introspection.
Combining Policies
Section titled “Combining Policies”Multiple protocols on the same router
Section titled “Multiple protocols on the same router”Pass multiple policies as template parameters to crateSetup. Every model added to the router gets endpoints for all configured protocols:
auto crateRouter = router.crateSetup!(RestApi, Mcp);crateRouter.add(productCrate);
// Product is now accessible via:// GET /products (REST)// POST /mcp (MCP: tools/call, name: "list_products")All four protocols at once:
auto crateRouter = router.crateSetup!(RestApi, JsonApi, Mcp, GraphQL);crateRouter.add(productCrate);Selecting policies per model
Section titled “Selecting policies per model”Not every model needs every protocol. Use prepareFor or addFor to pick which policies apply to a specific model:
auto crateRouter = router.crateSetup!(RestApi, Mcp, GraphQL);
// Spaces get all three protocolscrateRouter.add(spaceCrate);
// Users only get REST — no MCP tools, no GraphQLcrateRouter.prepareFor!(RestApi)(userCrate) .and(authMiddleware);
// Products get REST + MCP but no GraphQLcrateRouter.prepareFor!(RestApi, Mcp)(productCrate) .and(validationFilter);Selecting a policy that is not configured on the router is a compile error.
Swapping policies
Section titled “Swapping policies”Switching a model from one protocol to another is a one-line template change:
// Before: REST onlyauto crateRouter = router.crateSetup!RestApi;
// After: swap to JSON:APIauto crateRouter = router.crateSetup!JsonApi;
// The crate setup code stays exactly the samecrateRouter.add(productCrate);The Crate!T, middleware, and custom operations are protocol-agnostic. Only the policy determines how requests and responses are formatted.
Custom base URLs per protocol
Section titled “Custom base URLs per protocol”Each policy accepts a PolicyConfig for base URL customization:
auto crateRouter = router.crateSetup!( RestApiPolicy!(PolicyConfig("/api/v1")), McpPolicy!(PolicyConfig("/tools")), GraphQLPolicy!(PolicyConfig("/api")));crateRouter.add(productCrate);
// REST: GET /api/v1/products// MCP: POST /tools/mcp// GraphQL: POST /api/graphqlInternals
Section titled “Internals”CrateRule Structure
Section titled “CrateRule Structure”A CrateRule combines request matching with response configuration:
struct CrateRule { CrateRequest request; // Method, path, mime, parameters, bodyMatcher CrateResponse response; // Status code, mime, headers, serializer Schema[string] schemas; // OpenAPI schema definitions ModelDescription model; // Model metadata}CrateRequest Fields
Section titled “CrateRequest Fields”| Field | Purpose |
|---|---|
method | HTTP method (GET, POST, PUT, PATCH, DELETE) |
path | URL template (e.g., /users/:id) |
mime | Expected request content type |
parameters | Path/query parameters for OpenAPI docs |
requiredHeaders | Headers the client must send |
bodyMatcher | JSON pattern for body-based routing (MCP, GraphQL) |
CrateResponse Fields
Section titled “CrateResponse Fields”| Field | Purpose |
|---|---|
statusCode | HTTP response status (200, 201, 204, etc.) |
mime | Response content type |
headers | Response headers (with :id and :base_uri placeholders) |
serializer | Protocol-specific serializer instance |
How REST Creates Rules
Section titled “How REST Creates Rules”The REST policy generates path-based rules:
CrateRule getList(const ModelDescription definition) { auto rule = templateRule(definition); rule.request.path = config.baseUrl ~ "/" ~ definition.plural; rule.request.method = HTTPMethod.GET; rule.response.statusCode = 200; return rule;}The templateRule() method sets common defaults: serializer, required headers (Accept: application/json), and OpenAPI schema references.
How MCP Creates Rules
Section titled “How MCP Creates Rules”MCP uses a single endpoint with body-based routing:
CrateRule getItem(const ModelDescription definition) { string toolName = "get_" ~ definition.singular.toLower; auto rule = templateRule(definition, toolName); rule.request.path = config.baseUrl ~ "/mcp"; rule.request.method = HTTPMethod.POST; rule.request.bodyMatcher = toolCallMatcher(toolName); return rule;}All MCP operations share the same path and HTTP method. The bodyMatcher differentiates them.
How TypeRouter Discovers Policy Methods
Section titled “How TypeRouter Discovers Policy Methods”TypeRouter uses compile-time reflection to find all methods on a Policy that return CrateRule:
enum policyDescription = describe!Policy;
static foreach (method; policyDescription.methods) { static if (method.returns.name == "CrateRule" && method.name != "templateRule") {{ enum opName = operationClassFromMethod(method.name); // "getList" → "GetListApiOperation"
static if (__traits(compiles, mixin(newExprTyped))) { this.blankRouter.operations ~= mixin(newExprTyped); } else static if (__traits(compiles, mixin(newExprUntyped))) { this.blankRouter.operations ~= mixin(newExprUntyped); } }}}This is pure compile-time code generation — templateRule is excluded because it’s a helper, not an operation.
Lifecycle Hooks
Section titled “Lifecycle Hooks”Policies can optionally define lifecycle methods:
onRouterInit — Called once when the first model is added:
static void onRouterInit(ITypeRouter router) { // MCP registers /mcp initialize and tools/list endpoints here router.registerOperation(new McpInitializeOperation(...));}onRouterReady — Called lazily on the first HTTP request:
static void onRouterReady(IRouterContext router) { // MCP finalizes the tools list with all registered models toolsListOperation.setRouter(router);}CrateRouter checks if these methods exist at compile time and calls them only if defined.
Adding a New Policy
Section titled “Adding a New Policy”To create a custom policy:
- Define a struct with
staticmethods returningCrateRule - Follow the naming convention (
getList,getItem,createItem, etc.) - Set the
Serializeralias to your protocol’s serializer - Optionally implement
onRouterInitandonRouterReady
TypeRouter will automatically discover your methods and generate operations.