Skip to content

CQRS in TypeScript and Node.js with Cratis Arc

CQRS (Command Query Responsibility Segregation) means commands change state and queries read it, each with its own model. For TypeScript and Node.js, Cratis offers two separate pieces. Arc for TypeScript is a CQRS server with decorated commands and queries, but it is a source preview and no package is published to npm. @cratis/chronicle is the released Chronicle client for event sourcing. Choose with that difference in mind.

This page states plainly what exists. If you want a production CQRS backend today in TypeScript, Arc for TypeScript is not yet that. If you want to evaluate the model, or to build CQRS on the Chronicle event store from Node.js, read on.

PieceWhat it doesMaturity
Arc for TypeScriptCommands, queries, validation, authorization, tenancy and observable queries on Node.jsSource preview; not published to npm
Arc hostsStandalone host, Express, Fastify and Hono adaptersSource preview
PersistenceTenant-scoped MongoDB collections and Drizzle SQL queriesSource preview
Arc Chronicle integrationAppends events a command returns; serves projected read modelsExperimental
Frontend proxies@cratis/arc and @cratis/arc.react packages; proxies generated from backend sourceFrontend packages released; generator bounded
@cratis/chronicleChronicle client: events, reducers, reactors, projectionsReleased

Arc for TypeScript does not have full parity with Arc on .NET. Its capability reference lists each capability, the spec that proves it, and where it deliberately differs. Read it before you design around a feature.

In the Tasks sample, a command is a class with fields and a handle method. A query is a static method on a read model. These excerpts use TaskId, TaskTitle and Tasks from that sample:

@command()
export class RegisterTask {
@field(TaskId) id!: TaskId;
@field(TaskTitle) title!: TaskTitle;
handle(tasks: Tasks): TaskId {
tasks.register(this.id, this.title);
return this.id;
}
}
@readModel()
export class TaskItem {
@field(TaskId) id!: TaskId;
@field(TaskTitle) title!: TaskTitle;
@query(service(Tasks))
static allTasks(tasks: Tasks): TaskItem[] { return tasks.all(); }
}

@field comes from @cratis/fundamentals; the other decorators come from @cratis/arc.core. Arc decodes the fields, runs the command or query in a service scope, and serves it over the same HTTP contract as Arc on .NET. The sample’s artifacts are discovered from a Features/ folder, which gives routes such as POST /api/tasks/registration/register-task and GET /api/tasks/listing/all-tasks. The Arc for TypeScript README the Arc for TypeScript docs show the full setup, including how to build from source.

Validation runs before handle(), and a request that fails it never reaches your code. Observable queries stream results over SSE or WebSocket. Both are listed in the capability reference with their limits.

Arc does not need an event log. The Chronicle integration, which is experimental, makes handle() return the fact instead of writing it, and Arc appends it after authorization and validation succeed:

@eventType()
export class AuthorRegistered {
@field(AuthorName) name: AuthorName;
constructor(name: AuthorName = new AuthorName('')) { this.name = name; }
}
@command()
export class RegisterAuthor {
@key() @field(AuthorId) id!: AuthorId;
@field(AuthorName) name!: AuthorName;
handle(): AuthorRegistered { return new AuthorRegistered(this.name); }
}

A model-bound projection then builds the read model from the event, and the query reads it:

@readModel()
@fromEvent(AuthorRegistered)
export class Author {
@field(AuthorId) id!: AuthorId;
@field(AuthorName) name!: AuthorName;
@query()
static allAuthors(models: ChronicleReadModels): Promise<Author[]> {
return models.getAll(Author);
}
}

Storing the event and projecting it are separate steps. A query sent immediately after a command can read the model as it was before the event. The add event sourcing guide shows how to wait for the projection and covers the setup, including a local Chronicle kernel.

If you already have an HTTP layer, you can skip Arc and use the released client directly. Events, reducers, projections and reactors are decorated classes, and you append through store.eventLog.append. See event sourcing in TypeScript and Node.js for the install steps and a first example. You then write the command side yourself and read projected models through the client.

CQRS earns its cost when reads and writes have different shapes or load, or when you want events as the source of truth. For a CRUD API over one table it is overhead. Express or NestJS with an ORM is simpler there. Coming from Express and NestJS maps the concepts.

Arc for TypeScript is the wrong choice today if you need an npm-published, production-supported server, or full parity with the .NET feature set. In that case consider Arc on .NET or Kotlin and Java, or use the Chronicle client with your own command handling. See also CQRS explained.

Is there a CQRS framework for TypeScript and Node.js?

Section titled “Is there a CQRS framework for TypeScript and Node.js?”

Yes, in preview. Arc for TypeScript provides commands, queries, validation and authorization on Express, Fastify, Hono or a standalone host. Its source is public, but no package is published to npm and it does not have full .NET parity.

Can I do CQRS in Node.js with event sourcing?

Section titled “Can I do CQRS in Node.js with event sourcing?”

Yes. The @cratis/chronicle client is released and works on its own. Combining it with Arc through the Chronicle integration is experimental.

Does the frontend get typed commands and queries?

Section titled “Does the frontend get typed commands and queries?”

Yes. Arc’s proxy generator reads the decorated backend source and emits typed commands, queries and observable queries for the published @cratis/arc client. The generator is bounded; the capability reference lists what it covers.

Do I have to use TypeScript on both sides?

Section titled “Do I have to use TypeScript on both sides?”

No. The same frontend packages work with Arc backends in C# and Kotlin and Java. See Arc backends.

Compare the options in Event sourcing in TypeScript and Node.js: Chronicle with Arc.TypeScript, Emmett, and the KurrentDB Node client, including when an alternative is the better choice.