This repository demonstrates a small event-sourced system built with NestJS 11, CQRS, and PostgreSQL. It models a simple Invoices bounded context to show how to capture domain events, project read models, and expose an HTTP API with OpenAPI/Swagger documentation.
- Core module (
src/core) — Configures two PostgreSQL connections with TypeORM: one for the event store and one for the read database. Connection names are pulled from environment variables so you can point each store to separate databases. - Shared module (
src/shared) — Houses the event-sourcing building blocks:VersionedAggregateRootextends NestJSAggregateRootwith stream versioning for optimistic concurrency.PgEventStorepersists serialized domain events into theeventstable and can replay them by stream.- Event serializers/deserializers, publishers, and subscribers wire domain events into the NestJS CQRS event bus.
AggregateRehydratorloads aggregates from historical events to rebuild state.
- Invoices bounded context (
src/invoices) — Implements the example domain using CQRS:- Domain:
Invoiceaggregate reacts to events likeInvoicePaidEventand tracks its currentversionand status. - Application: Commands (
CreateInvoiceCommand,PayInvoiceCommand) and queries (FindInvoiceByIdQuery,FindAllInvoicesQuery) are executed viaCommandBus/QueryBusinInvoicesService. - Infrastructure: Repositories persist events and read models to PostgreSQL databases defined in the core module.
- Presentation:
InvoicesControllerexposes REST endpoints backed by DTO validation and documented through Swagger.
- Domain:
- API docs — Swagger is configured in
src/main.tsand served at/apionce the app is running.
src/
├─ core/ # Database connections for event store and read DB
├─ shared/ # Event store, aggregate base class, serializers, infrastructure
├─ invoices/ # Domain, application services, event handlers, HTTP controllers
└─ main.ts # App bootstrap and Swagger configuration
- Node.js 20+
- pnpm (
npm install -g pnpm) - Docker (optional but recommended for local PostgreSQL via
docker-compose)
pnpm installCreate a .env file in the project root defining the two database connections:
# Event store connection
EVENT_STORE_HOST=localhost
EVENT_STORE_PORT=5432
EVENT_STORE_USERNAME=postgres
EVENT_STORE_PASSWORD=postgres
EVENT_STORE_DATABASE=event_store
# Read database connection
READ_DB_HOST=localhost
READ_DB_PORT=5433
READ_DB_USERNAME=postgres
READ_DB_PASSWORD=postgres
READ_DB_DATABASE=read_db
# API
PORT=3001Adjust the ports/usernames/passwords if you already have PostgreSQL running locally.
If you do not have databases running, start the ones defined in docker-compose.yml:
docker-compose up -d pg-event-store pg-read-db# development mode with hot reload
pnpm run start:dev
# production build
pnpm run build
pnpm run start:prodOnce running, browse Swagger UI at http://localhost:3001/api to explore the REST endpoints.
# unit tests
pnpm run test
# watch mode
pnpm run test:watch
# coverage
pnpm run test:cov- Command dispatch: The controller calls
InvoicesService, which dispatches commands throughCommandBus. - Domain logic: Command handlers load aggregates via
AggregateRehydrator, invoke domain behaviors (e.g.,invoice.pay()), and emit domain events. - Event persistence:
PgEventStoreserializes events into theeventstable with a stream position for optimistic concurrency. - Projections: Event handlers update read models in the read database so queries stay fast and side-effect-free.
- Queries: Controllers use the
QueryBusto read from the projection layer without touching the write model.
This separation keeps the domain consistent while providing a clean, testable surface for both writes and reads.
POST /invoices— Create an invoice (customer ID and amount).GET /invoices— List projected invoices from the read database.GET /invoices/:id— Fetch a single invoice read model.POST /invoices/:id/pay— Pay an invoice; appends anInvoicePaidEventto the stream and updates projections.
- Ensure both PostgreSQL instances are reachable with the credentials in your
.envfile. - If schema tables are missing, confirm
synchronize: trueis acceptable for local development or create migrations for production. - Use
pnpm run lintto auto-fix lint issues before committing.
MIT