Skip to content

Policy System

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.

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:

MethodPathOperation
GET/usersList
GET/users/:idGet item
POST/usersCreate
PUT/users/:idReplace
PATCH/users/:idUpdate
DELETE/users/:idDelete (204)

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:

MethodPathOperation
GET/users/:id/relationships/:relGet relationship
PATCH/users/:id/relationships/:relUpdate relationship
POST/users/:id/relationships/:relAdd to relationship
DELETE/users/:id/relationships/:relRemove 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 /mcp with method: "initialize" — protocol handshake
  • POST /mcp with method: "tools/list" — list available tools
  • GET /mcp/sse — Server-Sent Events stream

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.

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);

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 protocols
crateRouter.add(spaceCrate);
// Users only get REST — no MCP tools, no GraphQL
crateRouter.prepareFor!(RestApi)(userCrate)
.and(authMiddleware);
// Products get REST + MCP but no GraphQL
crateRouter.prepareFor!(RestApi, Mcp)(productCrate)
.and(validationFilter);

Selecting a policy that is not configured on the router is a compile error.

Switching a model from one protocol to another is a one-line template change:

// Before: REST only
auto crateRouter = router.crateSetup!RestApi;
// After: swap to JSON:API
auto crateRouter = router.crateSetup!JsonApi;
// The crate setup code stays exactly the same
crateRouter.add(productCrate);

The Crate!T, middleware, and custom operations are protocol-agnostic. Only the policy determines how requests and responses are formatted.

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/graphql

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
}
FieldPurpose
methodHTTP method (GET, POST, PUT, PATCH, DELETE)
pathURL template (e.g., /users/:id)
mimeExpected request content type
parametersPath/query parameters for OpenAPI docs
requiredHeadersHeaders the client must send
bodyMatcherJSON pattern for body-based routing (MCP, GraphQL)
FieldPurpose
statusCodeHTTP response status (200, 201, 204, etc.)
mimeResponse content type
headersResponse headers (with :id and :base_uri placeholders)
serializerProtocol-specific serializer instance

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.

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.

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.

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.

To create a custom policy:

  1. Define a struct with static methods returning CrateRule
  2. Follow the naming convention (getList, getItem, createItem, etc.)
  3. Set the Serializer alias to your protocol’s serializer
  4. Optionally implement onRouterInit and onRouterReady

TypeRouter will automatically discover your methods and generate operations.