Tutorial: Build a Complete API
This tutorial walks you through building a complete API with Crate, from installing D to running a multi-model service with authentication, middleware, and custom operations. No prior D experience required.
By the end you will have a project that looks like a real production service: a service class, configuration files, multiple models in separate packages, query filters, response mappers, and a custom publish endpoint.
Installing D
Section titled “Installing D”D compiles to native code and has a package manager called dub. Install both:
Linux (most distros):
curl -fsS https://dlang.org/install.sh | bash -s dmdThis installs the D compiler (dmd) and dub. Follow the on-screen instructions to add them to your PATH.
macOS:
brew install dmdWindows:
Download the installer from dlang.org/download. The installer includes both dmd and dub.
Verify the installation:
dmd --versiondub --versionScaffold the Project
Section titled “Scaffold the Project”Crate includes an init template that generates a complete project structure interactively:
mkdir blog-api && cd blog-apidub init -t crateThe wizard asks for:
- Project name —
blog-api - Description —
A blog API built with Crate - Number of models —
2 - Model names —
Article, thenCategory - Storage backend —
mongo - Protocols —
rest - Authentication —
yes - Port —
9090
This generates the full project tree:
blog-api/ source/ app.d # Entry point blog_api/api/ api.d # Central route registration configuration.d # Config struct service.d # WebService class packages/ articles/ dub.json source/blog_api/articles/ model.d # Article struct api.d # Route setup middleware/filter.d # Query filter operations/count.d # Custom operation tests/ getList.d, getItem.d, post.d, delete_.d categories/ ... # Same layout config/ configuration.json # Runtime config configuration.model.json # Reference template db/mongo.json # MongoDB connection about/ name.txt, version.txt, description.txt dub.json # Root build config .gitignoreEvery model lives in its own packages/ sub-package. The source/ directory contains only the entry point, service class, configuration, and route registration. This separation keeps each feature self-contained.
Understanding the Entry Point
Section titled “Understanding the Entry Point”Open source/app.d:
import blog_api.api.service;import crate.service.main;
int main(string[] args) { return mainService!(ApiService)(args);}mainService reads configuration from config/configuration.json, instantiates your ApiService, and calls its main() method. You start the server with:
dub run -- startThe -- start argument tells the service to boot the HTTP server.
The Service Class
Section titled “The Service Class”Open source/blog_api/api/service.d. The service class extends WebService and is where you set up the HTTP server, connect to the database, and register routes:
class ApiService : WebService!ApiConfiguration { override int main() { auto router = new URLRouter; // connect crates, register routes, start listening return runEventLoop(); }}WebService!T handles configuration loading automatically. Your ApiConfiguration struct (in configuration.d) maps 1:1 to config/configuration.json:
struct ApiConfiguration { GeneralConfig general; HTTPConfig http;}You add fields to this struct as your project grows — each field maps to a key in the JSON file.
Models
Section titled “Models”Open packages/articles/source/blog_api/articles/model.d. A model is a D struct:
struct Article { string _id; string title; string content;}The _id field is the primary key. Crate introspects the struct at compile time to generate endpoints, serialization, and validation.
Optional Fields
Section titled “Optional Fields”Mark fields that don’t need to be provided on creation with @optional:
import vibe.data.serialization : optional;
struct Article { string _id; string title; string content;
@optional { string[] tags; string status; SysTime publishedAt; }}Fields inside @optional { } can be omitted in POST requests. They’ll be zero-initialized (empty string, empty array, etc).
Nested Types
Section titled “Nested Types”Models can contain other structs:
struct Visibility { string teamId; bool isPublic;}
struct Article { string _id; string title; string content; Visibility visibility;
@optional string[] tags;}Crate serializes nested structs as nested JSON objects automatically.
Connecting to MongoDB
Section titled “Connecting to MongoDB”In your service class, crates are created from MongoDB collections:
import crate.collection.mongo;
auto articles = new MongoCrate!Article(client, "blog.articles");auto categories = new MongoCrate!Category(client, "blog.categories");The first argument is a MongoDB client (obtained from your config), the second is the database.collection name. MongoCrate implements the same interface as MemoryCrate, so you can swap storage backends without changing any other code.
Route Registration
Section titled “Route Registration”Open source/blog_api/api/api.d. This is where all routes come together:
void setupApi(T)(CrateRouter!T crateRouter, /* crates, config, etc. */) { setupArticleApi(crateRouter, crates); setupCategoryApi(crateRouter, crates);}Each feature package exports a setup*Api function. Inside packages/articles/source/blog_api/articles/api.d:
void setupArticleApi(T)(CrateRouter!T crateRouter, Crate!Article articleCrate) { crateRouter.add(articleCrate);}This single line generates six endpoints:
| Method | Path | Operation |
|---|---|---|
| GET | /articles | List all |
| GET | /articles/:id | Get one |
| POST | /articles | Create |
| PUT | /articles/:id | Replace |
| PATCH | /articles/:id | Update |
| DELETE | /articles/:id | Delete |
Adding Authentication
Section titled “Adding Authentication”Crate integrates with vibe-auth for OAuth2. In your central api.d:
import crate.auth.usercollection;import vibeauth.authenticators.OAuth2;
auto collection = new UserCrateCollection([], userCrate);auto auth = new OAuth2(collection);
crateRouter.router.crateSetup.enable(auth);This protects all routes globally. For finer control, you can apply auth as middleware on specific models (covered below).
Public vs Private Endpoints
Section titled “Public vs Private Endpoints”Use different middleware classes to control access per model:
import crate.auth.middleware;
auto publicAuth = new PublicDataMiddleware(collection, null, OAuth2Configuration());auto privateAuth = new PrivateDataMiddleware(collection, null, OAuth2Configuration());
// Articles: readable without auth, writable with authcrateRouter.prepare(articleCrate).and(publicAuth);
// Categories: always requires authcrateRouter.prepare(categoryCrate).and(privateAuth);PublicDataMiddleware allows unauthenticated reads but requires a token for writes. PrivateDataMiddleware requires a token for everything.
Middleware
Section titled “Middleware”Middleware classes intercept requests at specific points. They use annotations to declare which operations they apply to.
Query Filters
Section titled “Query Filters”A filter modifies the database query based on request parameters. Create packages/articles/source/blog_api/articles/middleware/filter.d:
class TagFilter { struct Parameters { @describe("Filter articles by tag. Only articles containing this tag will be returned.") @example("dlang", "Return only articles tagged with `dlang`.") @example("tutorial", "Return only articles tagged with `tutorial`.") string tag; }
@get IQuery get(IQuery selector, Parameters parameters) { if (parameters.tag == "") return selector; return selector.where("tags").arrayContains(parameters.tag).and; }}The @get annotation means this runs on GET list and GET item requests. The nested Parameters struct declares which query parameters this filter accepts — Crate parses them from the URL automatically at compile time. No manual req.params.get(...) needed.
The @describe and @example annotations are picked up by OpenAPI spec generation, so your API docs stay in sync with the code. A request to /articles?tag=dlang only returns articles whose tags array contains "dlang".
Response Mappers
Section titled “Response Mappers”A mapper decorates each item in the response. Useful for computed fields:
class ArticleStatsMapper { private Crate!Metric metricCrate;
this(Crate!Metric metricCrate) { this.metricCrate = metricCrate; }
@mapper void addViewCount(HTTPServerRequest req, ref Json item) { item["viewCount"] = metricCrate.get .where("articleId").equal(item["_id"].to!string).and .size; }}The @mapper annotation runs this on every item returned by GET requests. It adds a viewCount field computed from a separate collection.
Validators
Section titled “Validators”Validate or transform data before it hits the database:
class SlugMiddleware { @create void setSlug(HTTPServerRequest req) { auto json = req.json; if ("slug" !in json["article"] || json["article"]["slug"].to!string == "") { json["article"]["slug"] = json["article"]["title"].to!string .toLower.replace(" ", "-"); } }}@create runs on POST requests. @put runs on PUT. You can combine them: a method annotated @create @put runs on both.
Wiring Middleware
Section titled “Wiring Middleware”Back in api.d, chain middleware with .prepare() and .and():
void setupArticleApi(T)(CrateRouter!T crateRouter, Crate!Article articleCrate) { auto tagFilter = new TagFilter; auto slugMiddleware = new SlugMiddleware; auto statsMapper = new ArticleStatsMapper(metricCrate);
crateRouter .prepare(articleCrate) .and(tagFilter) .and(slugMiddleware) .and(statsMapper);}Middleware runs in the order it’s added. Filters narrow the query, validators check/transform the payload, and mappers decorate the response.
Custom Operations
Section titled “Custom Operations”Standard CRUD covers most endpoints, but sometimes you need something specific — like publishing an article. Custom operations add new routes to an existing model.
Create packages/articles/source/blog_api/articles/operations/publish.d:
import crate.base;
class ArticlePublishOperation : CrateOperation { private Crate!Article crate;
this(Crate!Article crate) { CrateRule rule; rule.request.path = "/articles/:id/publish"; rule.request.method = HTTPMethod.POST; super(crate, rule); this.crate = crate; }
override void handle(OperationContext storage) { auto item = this.prepareItemOperation!"getItem"(storage);
item["status"] = "published"; item["publishedAt"] = Clock.currTime.toISOExtString; crate.updateItem(item);
storage.response.statusCode = 200; storage.response.writeJsonBody(item); }}Register it in the route setup:
auto publishOp = new ArticlePublishOperation(articleCrate);
crateRouter .prepare(articleCrate) .withCustomOperation(publishOp) .and(tagFilter) .and(slugMiddleware);This adds POST /articles/:id/publish alongside the auto-generated CRUD routes.
Configuration
Section titled “Configuration”The config/ directory contains runtime configuration that the framework loads automatically:
config/configuration.json — main config, maps to your ApiConfiguration struct:
{ "general": { "serviceName": "blog-api", "apiUrl": "http://localhost:9090", "db": "./config/db" }, "http": { "hostName": "0.0.0.0", "port": 9090 }}config/db/mongo.json — MongoDB connection:
{ "type": "mongo", "url": "mongodb://127.0.0.1:27017/blog", "configuration": { "database": "blog", "maxConnections": 10 }}Add new fields to your configuration struct as needed. The framework deserializes the JSON directly into the struct — no parsing code required.
Environment Variables
Section titled “Environment Variables”Hardcoding values in configuration.json works for local development, but in production you want to inject settings from the environment. Crate supports three substitution syntaxes that are resolved before the JSON is deserialized into your struct:
| Syntax | Example | Result |
|---|---|---|
"$VAR" | "$apiUrl" | Replaced with the env var value as a string |
"#VAR" | "#logLevel" | Replaced with the env var value as an integer |
"{$A}...{$B}" | "{$host}:{#port}" | Inline interpolation — each {...} segment is resolved and concatenated |
If an env var is not set, the service fails to start with a clear error message. This is intentional — missing configuration should be caught immediately, not silently default to empty strings.
Create a config/configuration.docker.json for deployment:
{ "general": { "serviceName": "$serviceName", "apiUrl": "$apiUrl", "logLevel": "#logLevel", "db": "./config/db" }, "http": { "hostName": "$HOSTNAME", "port": "#httpPort" }}And a config/db.model/mongo.json:
{ "type": "mongo", "url": "$mongoUrl", "configuration": { "database": "$mongoDBName" }}The .model suffix is a convention — these are reference templates that get copied into place during container build. The actual config/db/mongo.json used at runtime is gitignored. See Deploying for Docker, Compose, and Helm setup.
Running and Testing
Section titled “Running and Testing”Build and start the server:
dub builddub run -- startTest with curl:
# Create an articlecurl -X POST http://localhost:9090/articles \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{"article": {"title": "Hello World", "content": "My first post", "tags": ["intro"]}}'
# List articlescurl http://localhost:9090/articles
# Filter by tagcurl http://localhost:9090/articles?tag=intro
# Publish an articlecurl -X POST http://localhost:9090/articles/000000000000000000000001/publish \ -H "Authorization: Bearer <token>"Run the test suite:
dub testTests use in-memory crates (MemoryCrate) so they don’t need a running database. Each test file spins up a router, seeds test data, and makes HTTP requests against it.
Project Structure Recap
Section titled “Project Structure Recap”The pattern used in this tutorial is the same pattern used in production Crate services:
source/ # Thin entry point + wiring app.d # mainService!(ApiService)(args) blog_api/api/ service.d # WebService subclass configuration.d # Config struct api.d # Central setupApi() functionpackages/ # One sub-package per feature articles/ source/blog_api/articles/ model.d # Data struct api.d # setupArticleApi() — route + middleware wiring middleware/ # Filters, validators, mappers operations/ # Custom non-CRUD endpoints tests/ # Per-feature testsconfig/ # Runtime configuration (not compiled) configuration.json db/mongo.jsonEach feature is a self-contained dub sub-package with its own model, route setup, middleware, operations, and tests. The central api.d calls each feature’s setup function. This scales cleanly — adding a new model means adding a new package directory without touching existing code.
Next Steps
Section titled “Next Steps”- Models & CRUD — field types, relations, and CRUD behaviour in detail
- Middleware — full reference for all middleware annotations
- Custom Operations — advanced patterns for non-CRUD routes
- Authentication — OAuth2 setup, user management, and role-based access