Skip to content

Creating a Custom Crate

Every storage backend implements Crate!T, which extends CrateAccess:

interface CrateAccess {
@safe:
IQuery get(); // Return a query over all items
IQuery getItem(const string id); // Return a query for one item by ID
Json addItem(const Json item); // Insert item, return it with generated ID
Json updateItem(const Json item); // Replace item by ID, return updated
void deleteItem(const string id); // Remove item by ID
}
interface Crate(Type) : CrateAccess {
}

To create a custom crate, implement Crate!T for your model type and provide a matching IQuery implementation.

Here’s a crate backed by a simple associative array:

import crate.base;
import vibe.data.json;
class HashMapCrate(T) : Crate!T {
private {
Json[string] store;
ulong nextId;
}
IQuery get() @trusted {
return new ArrayQuery(store.values);
}
IQuery getItem(const string id) @trusted {
if (id in store) {
return new ArrayQuery([store[id]]);
}
return new ArrayQuery([]);
}
Json addItem(const Json item) @trusted {
auto data = item.clone;
auto id = (++nextId).to!string;
data["_id"] = id;
store[id] = data;
return data;
}
Json updateItem(const Json item) @trusted {
auto id = item["_id"].get!string;
store[id] = item.clone;
return store[id];
}
void deleteItem(const string id) @trusted {
store.remove(id);
}
}

Your crate’s get() and getItem() methods must return an IQuery. This is the query builder interface that operations use for filtering, sorting, and pagination:

interface IQuery {
@safe:
IFieldQuery where(string field); // Start a field filter
IQuery sort(string field, int order); // 1 = ascending, -1 = descending
IQuery limit(size_t nr); // Limit result count
IQuery skip(size_t nr); // Skip N results (pagination)
InputRange!Json exec(); // Execute and return results
size_t size(); // Count without fetching
}

where() returns IFieldQuery for building field conditions:

interface IFieldQuery {
@safe:
IFieldQuery equal(string value);
IFieldQuery greaterThan(SysTime time);
IFieldQuery lessThan(SysTime time);
IFieldQuery like(string pattern);
IFieldQuery arrayContains(string value);
IFieldQuery anyOf(string[] values);
IQuery and(); // Return to the parent query
}

For a simple crate, you can use MemoryQuery from crate.collection.memory — it implements IQuery over a Json[] array with in-memory filtering.

Accept an optional CrateConfig!T in your constructor to let users disable specific operations:

class HashMapCrate(T) : Crate!T {
private CrateConfig!T _config;
this(CrateConfig!T config = CrateConfig!T()) {
_config = config;
}
}

The framework reads CrateConfig when generating routes — if config.deleteItem is false, no DELETE endpoint is created.

Use your custom crate the same way you’d use MemoryCrate or MongoCrate:

auto crate = new HashMapCrate!User;
auto router = new URLRouter();
router.crateSetup!RestApi
.add(crate, authMiddleware);

The framework only cares that your crate implements Crate!T. Everything else — route generation, middleware, serialization — works automatically.

For database-backed crates, handle connection setup in the constructor:

class RedisCrate(T) : Crate!T {
private RedisClient client;
this(string host, ushort port = 6379) {
this.client = connectRedis(host, port);
}
}

Crate expects items to have a string _id field after insertion. Your addItem must generate and assign an ID:

Json addItem(const Json item) @trusted {
auto data = item.clone;
data["_id"] = randomUUID().toString; // Or ObjectId, or auto-increment
// ... store it ...
return data;
}

Use describeModel!T if you need compile-time information about the model’s fields:

class SmartCrate(T) : Crate!T {
enum description = describeModel!T;
enum idField = getIdField!description.name; // "_id" or custom
Json addItem(const Json item) @trusted {
auto data = item.clone;
data[idField] = generateId();
// ...
}
}

This is how MemoryCrate and MongoCrate discover the ID field name at compile time.