Skip to content

Custom Operations

The simplest way to add custom endpoints is to define methods directly on your model struct. Crate calls these “actions” and auto-discovers them when you use prepare() or add().

A method that returns a value becomes a GET endpoint:

struct User {
string _id;
string firstName;
string lastName;
string getFullName() {
return firstName ~ " " ~ lastName;
}
}

This creates GET /users/:id/getFullName which returns the user’s full name as a string.

A method that takes a parameter becomes a POST endpoint:

struct User {
string _id;
string name;
string setName(string newName) {
name = newName;
return name;
}
}

This creates POST /users/:id/setName. The request body is the parameter value.

An action can also accept an HTTPServerRequest for full request access:

import vibe.http.server : HTTPServerRequest;
struct Report {
string _id;
string data;
string handleRequest(HTTPServerRequest request) {
// Access headers, query params, etc.
return "processed";
}
}

By default, prepare() and add() auto-discover and expose all action methods on a model. If you want to control which actions are exposed, use using() and manually call .expose!:

auto typeRouter = crateRouter.using(userCrate);
// Only expose specific actions
typeRouter.expose!"getFullName";
typeRouter.expose!"setName";
// handleRequest is NOT exposed

For more control, use .itemOperation!() to define custom endpoints with explicit handlers:

auto typeRouter = crateRouter.prepare(reportCrate);
// Define a custom GET operation
@mime("custom/mime")
string generatePdf() {
return "pdf-data";
}
typeRouter.itemOperation!("generate-pdf")(&generatePdf);

This creates GET /reports/:id/generate-pdf.

For POST operations, the handler takes a parameter:

@mime("application/json")
int calculate(int value) {
return value * 2;
}
typeRouter.itemOperation!("calculate")(&calculate);

This creates POST /reports/:id/calculate.

For endpoints that don’t follow the item pattern, use .withCustomOperation() with a class that extends ApiOperation:

import crate.http.operations;
class HealthCheckOperation : ApiOperation {
this() {
CrateRule rule;
rule.request.path = "/health";
rule.request.method = HTTPMethod.GET;
rule.response.statusCode = 200;
super(rule);
}
override void handle(OperationContext ctx) {
ctx.response.writeJsonBody(Json(["status": Json("ok")]));
}
}

Register it on a router:

crateRouter
.prepare(someCrate)
.withCustomOperation(new HealthCheckOperation);

Or on a blank router (no model attached):

crateRouter
.blank()
.withCustomOperation(new HealthCheckOperation);

A common pattern is adding a history/audit endpoint to a model:

class HistoryOperation : ApiOperation {
this(string modelPath) {
CrateRule rule;
rule.request.path = modelPath ~ "/:id/history";
rule.request.method = HTTPMethod.GET;
rule.response.statusCode = 200;
super(rule);
}
override void handle(OperationContext ctx) {
auto id = ctx.request.params["id"];
// Fetch history from audit log...
ctx.response.writeJsonBody(historyData);
}
}
crateRouter
.prepare(campaignCrate)
.withCustomOperation(new HistoryOperation("/campaigns"));