DEV Community

Cover image for A NestJS reference app that proves six nest-native libraries under realistic backend pressure
Rodrigo Nogueira
Rodrigo Nogueira

Posted on • Edited on • Originally published at nest-native.dev

A NestJS reference app that proves six nest-native libraries under realistic backend pressure

TL;DR: nest-native/reference-app is a production-shaped multi-tenant work-tracking SaaS — think a small Linear — that composes all six nest-native libraries the way a real product would: Drizzle persistence, a typed tRPC API, domain events emitted in the same transaction as the writes, a Kafka backbone with exactly-once consumer effects, a published AsyncAPI 3.0 catalog, and a streaming AI assistant. Every guarantee has a test. It runs with zero infrastructure by default (SQLite, no broker, offline AI), and the same domain code runs against real Kafka by setting one env var.

What changed since this post first went up

This post originally covered a two-library app (nest-drizzle-native + nest-trpc-native). Since then:

Everything below describes the app as it is today.

The org, in one paragraph

nest-native publishes decorator-first NestJS integrations that should feel like official NestJS packages — modules, DI, decorators, guards/interceptors, lifecycle hooks — while staying honest about the tool underneath. Drizzle stays SQL-first; tRPC stays tRPC; the outbox is a real table, not magic. Every package ships with "dependencies": {} (you install only the peers you use), 100% test coverage as a hard CI gate, runnable samples, and documented non-goals.

The problem this app exists to solve

Library docs cover their slice; nobody covers the seams. And the seams are where backends actually get built:

  • How does "current user / current organization" reach a tRPC procedure, a guard, and a repository three calls deep — through one request-scoped mechanism?
  • How do I write a row and publish an event without a crash window between them?
  • How do consumers survive Kafka's at-least-once redelivery without double-applying effects?
  • Where do other teams find my event contracts — and how do I keep those contracts from drifting apart across producer, consumer, and docs?
  • How do I ship a streaming AI endpoint that tests offline, deterministically, in CI?

The reference app answers all of these in one codebase, as a coherent product rather than a pile of demos.

The story: one journey through six libraries

The product is a multi-tenant work-tracking SaaS. Follow one journey and every library shows up exactly where a real system would reach for it:

  1. An org invites a teammate — one @Transactional() method writes the user, membership, project, and audit rows and enqueues a user.invited event, atomically.
  2. They work tasks — create → assign → complete; each write emits task.created / task.assigned / task.completed in the same transaction.
  3. Events flow over the backbone — a background claimer relays committed events (in-process by default; Kafka in production), and consumers build an activity feed read-model, deduplicated against redelivery.
  4. The contracts are published — an AsyncAPI 3.0 catalog at /asyncapi documents every event, generated from the same Zod schemas the code validates with.
  5. AI reads the activity — a streaming assistant turns a project's recent activity into a status update, token by token.

reference-app as an architecture city: a central reference-app building wires the feature modules (auth, users, projects, tasks, activity, onboarding, audit, outbox, inbox, events-catalog, assistant, trpc, db) via glowing teal paths; two red arcs trace the cross-cutting concerns — transactions and auth request context

Library Its job in the story
@nest-native/drizzle Persistence: repositories (@DrizzleRepository, @InjectTransaction), tenant-scoped queries, migrations
@nest-native/trpc The typed API: @Router/@Query/@Mutation, generated AppRouter, superjson, guards
@nest-native/messaging Reliable events: transactional outbox + idempotent inbox on Drizzle
@nest-native/kafka The backbone: KafkaOutboxTransport + @KafkaConsumer read-models
@nest-native/asyncapi The event catalog other teams integrate against
@nest-native/ai-sdk The streaming assistant (@AiStream), offline-testable

The central proof: five writes, one transaction, zero lost events

The onboarding workflow is still the crown jewel. inviteUser() writes five rows — user, membership, project link, audit event, and the outbox event — inside a single @Transactional() method:

inviteUser as a pipeline: five numbered steps — users, memberships, projects, audit_events, outbox_events — flow through a transaction; a commit valve releases the queued outbox event to the worker

@Transactional()
inviteUser(input: InviteUserInput) {
  const user = this.users.create(...);
  this.memberships.create(...);
  // ...audit row...
  this.outbox.enqueue({
    topic: 'user.invited',
    payload,                        // a plain typed interface — no casts
    idempotencyKey: `user.invited:${orgId}:${user.id}`,
  });
}
Enter fullscreen mode Exit fullscreen mode

A throw anywhere rolls back everything — no phantom event. A crash after commit loses nothing — the event is durably in the database. The tests prove both directions (happy path, rollback safety, crash recovery), because "trust me" is not a delivery guarantee.

No lost events, no double effects

The dual-write problem ("write the row, then publish — and pray nothing crashes in between") is solved by @nest-native/messaging: the enqueue above is just an insert into an outbox_events table, and a background claimer relays committed rows with retry/backoff and stuck-claim recovery:

The outbox lifecycle: a worker picks up envelopes from a

The consumer half is the mirror image: delivery is at-least-once by contract, so the idempotent inbox dedups redeliveries via a unique (source, message_key) row written in the same transaction as the side effect — exactly-once effects, provable in the database. The app's activity feed is built exactly this way, and the test suite redelivers messages on purpose and asserts the side effect ran once.

Two profiles, one codebase:

  • Default — no broker: the outbox dispatches through an in-process topic→handler registry (@nest-native/messaging/in-process). SQLite in a file. This is what CI runs.
  • Kafka — set KAFKA_BROKERS and the same domain code relays through KafkaOutboxTransport, with @KafkaConsumers (via @nest-native/kafka, on Confluent's official client) on the other side. Same event bodies, same dedup keys, same wire headers.

A typed API that doesn't lie

The tRPC layer (@nest-native/trpc) is decorator-first (@Router('tasks'), @Query, @Mutation, Zod I/O) with a generated AppRouter consumed by a typed client in CI. Three details worth stealing:

  • superjson end-to-endactivity.list returns real Date objects, and the generated router carries a type-level transformer marker: a client that forgets to configure superjson fails to compile. Both directions are CI steps.
  • Validation errors clients can use — an errorFormatter flattens ZodErrors so a failing mutation surfaces error.data.zodError.fieldErrors.title on the typed client.
  • Cache headers where they're saferesponseMeta sets cache-control only on the public ping query; everything tenant-scoped stays uncached, and tests assert both presence and absence.

Your events, documented

@nest-native/asyncapi publishes an AsyncAPI 3.0 document at /asyncapi (plus -json/-yaml) describing all four domain events. The payload schemas are defined once as Zod objects — the same definitions derive the TypeScript types (z.infer), the runtime guards (safeParse), and the catalog schemas. Producer, consumer, and documentation cannot drift apart, and the document validates against @asyncapi/parser in the test suite.

AI over your data, tested offline

POST /projects/:id/assistant streams a status update summarizing the project's recent activity, via @nest-native/ai-sdk's @AiStream (SSE, abort on client disconnect). By default it streams from an offline mock model built from the real activity digest — deterministic, no API key, CI-safe. Set OPENAI_API_KEY and a real provider swaps in with no code change. (Adopting the AI SDK is also why the app requires Node ≥ 22ai@7 demands it.)

Try it in 30 seconds

git clone https://github.com/nest-native/reference-app && cd reference-app
nvm use            # Node >= 22
npm install
DATABASE_URL=./reference-app.db npm run db:migrate
DATABASE_URL=./reference-app.db npm run seed
AUTH_SECRET=dev-secret-must-be-at-least-32-characters-xxxxx \
  DATABASE_URL=./reference-app.db npm run start:dev
Enter fullscreen mode Exit fullscreen mode
  • tRPC at /trpc, health at /health, the AsyncAPI catalog at /asyncapi, the AI assistant at POST /projects/1/assistant
  • Seed login: admin@acme.test / admin123!
  • The outbox worker runs as its own process: npm run start:worker
  • No Docker, no broker, no API keys required for any of it

The loop that makes it honest

The app isn't just a showcase — it's the feedback loop for the libraries, and it has teeth. Recent examples, all shipped upstream because building this app surfaced them:

  • The in-process outbox transport was hand-rolled here first, proved generic, and was extracted into @nest-native/messaging/in-process.
  • The app's hand-written AI mock became @nest-native/ai-sdk/testing (createMockLanguageModel) — and deleted every copy-pasted mock in the family.
  • enqueue() becoming generic (killing as unknown as Record<string, unknown> casts), the in-memory Kafka broker gaining an awaitable idle() (killing sleep(50) in tests), and AsyncAPI accepting Zod schemas directly — all of it started as friction in this codebase.
  • The app's install even caught a broken peer-dependency range in a fresh release before any user hit it.

If something feels awkward here, that's treated as a library bug — fixed upstream, in its own PR, with its own tests.

What it deliberately is not

  • Not a starter kit or boilerplate — it optimizes for being read, not forked. Copy patterns, not the repo.
  • Not 100%-covered — the 100% bar belongs to the libraries; the app's suite is pragmatic and targets the guarantees (rollback, crash recovery, dedup, catalog validity, stream framing).
  • Not a CDC system — the outbox is the app-level answer; Debezium-style log tailing is a different trade-off and an explicit non-goal.
  • Not affiliated with the NestJS core team.

Where to find it

Feedback, issues, and "this seam is still awkward" reports are the most useful thing you can send — that's literally the mechanism this whole stack improves by.

Top comments (0)