Skip to content

Get started with MongoDB

This page connects one read model to MongoDB and serves it from three queries: a list, a page, and a live list. It follows the package’s replica-set integration fixture. You need a MongoDB server; the live query needs a replica set.

TaskRecord.ts
import { field, Guid } from '@cratis/fundamentals';
import { key } from '@cratis/arc.core';
export class TaskRecord {
@field(Guid) @key() id!: Guid;
@field(String) title!: string;
}

The collection reads and writes only the fields you declare with @field. @key() marks the field stored as _id; without it, a field named id is used, and a model with neither is rejected.

TaskQueries.ts
import { query, queryOptions, readModel, service, type QueryOptions } from '@cratis/arc.core';
import { mongoCollection, type MongoCollection } from '@cratis/arc.mongodb';
import { TaskRecord } from './TaskRecord.js';
const tasks = mongoCollection(TaskRecord);
@readModel()
export class TaskQueries {
@query(service(tasks))
static async all(items: MongoCollection<TaskRecord>): Promise<TaskRecord[]> {
return items.find();
}
@query(service(tasks), queryOptions())
static async page(items: MongoCollection<TaskRecord>, options: QueryOptions) {
return items.queryPage({}, options);
}
@query({ observable: true }, service(tasks))
static changes(items: MongoCollection<TaskRecord>) {
return items.observe();
}
}

mongoCollection(TaskRecord) is a service token. Each request resolves it to the collection in the current tenant’s database, so a query never chooses a database itself. all returns every task, page pushes count, sort, and paging into MongoDB (see Paging), and changes opens a change stream (see Observing collections).

For an owner-restricted query, build the filter from the verified principal, such as items.find({ owner: principal.id }), instead of forwarding a caller-provided object.

main.ts
import { ArcApplication, TenantResolverType } from '@cratis/arc.core';
import { MongoClient } from 'mongodb';
import '@cratis/arc.mongodb';
import { TaskRecord } from './TaskRecord.js';
import { TaskQueries } from './TaskQueries.js';
const client = new MongoClient(process.env.MONGODB_URI ?? 'mongodb://127.0.0.1:27017');
const builder = ArcApplication.createBuilder({ tenancy: { resolverType: TenantResolverType.Fixed, fixedTenantId: 'default' } });
builder.add(TaskQueries).withMongoDB({
client, databaseNameResolver: tenant => `tasks_${tenant}`, readModels: [TaskRecord]
});
const app = await builder.build();
await app.run();
await client.close();

Importing @cratis/arc.mongodb adds withMongoDB to the builder. The exported withMongoDB(builder, options) function is equivalent. List every model you inject in readModels; a model left out has no collection token.

The fixed default tenant makes this a single-tenant example, and every request uses the tasks_default database. See Tenancy for real tenant routing.

A GET on the all query’s route answers with the stored tasks. Endpoint mapping explains how routes are derived.

Configure the connection in appsettings.json

Section titled “Configure the connection in appsettings.json”

withMongoDB reads Cratis:MongoDB from the application’s configuration, so the server address and database can stay out of code:

appsettings.json
{
"Cratis": {
"MongoDB": {
"server": "mongodb://127.0.0.1:27017",
"database": "tasks"
}
}
}
main.ts (excerpt)
builder.add(TaskQueries).withMongoDB({ readModels: [TaskRecord] });

Only server and database bind from configuration; set everything else in code. Cratis__MongoDB__Server and Cratis__MongoDB__Database override the file in a deployment. Values in code win over configuration, and a client, server, or serverResolver in code replaces a configured server. With server, Arc creates the client and closes it when the application is disposed.

OptionMeaning
clientA caller-owned MongoClient; Arc leaves it open
serverA MongoDB URI; Arc creates the client and closes it with the application
serverResolver(tenantId, context)A URI per tenant, for tenants on different servers
databaseThe default database name; other tenants use <database>+<tenant>
databaseNameResolver(tenantId, context)A database name per tenant
readModelsModel classes registered for injection and command read-model resolution
namingPolicy, collectionName, ignoreConventionsSee Naming policies and Serializers
maxPageSizePage size cap, 100 by default, at most 10,000
maxObservableItemsObserved snapshot cap, 1,000 by default, at most 10,000

Specify exactly one of client, server, or serverResolver, and either database or databaseNameResolver. A missing tenant or empty database name fails the request rather than reading an implicit default database.

The collection’s query methods read. To write from a command, inject the same token and write through the driver collection, encoding the model first:

await items.native.insertOne(items.codec.serialize(task));

Encoding through codec keeps the stored document in the shape reads expect. See Serializers.