Skip to content

About

A batteries-included NestJS 11 starter (Fastify, PostgreSQL/TypeORM, Redis, BullMQ) built to be forked into new services.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

NestJS Backend Boilerplate

An opinionated NestJS 11 backend skeleton — PostgreSQL, Redis cache, BullMQ queues, strict validation, and a complete example feature — ready to fork into new services.

NestJS TypeScript Fastify PostgreSQL Redis Node License


📑 Table of Contents


✨ Features

  • ⚡ Fastify HTTP adapter (not Express).
  • 🗄️ PostgreSQL + TypeORM with entity-first, script-driven migrations & seeders.
  • 🧊 Redis cache via @nestjs/cache-manager, with an in-memory fallback for dev.
  • 🐂 BullMQ queue root connection on its own, separate Redis.
  • 🛡️ Fail-fast config — every *_* env var validated at boot with class-validator.
  • 🧬 Strict DTOs — whitelist + forbidNonWhitelisted + transform globally.
  • 📡 Observability — structured pino logs (Loki-ready) with trace correlation, toggleable Prometheus metrics, and toggleable OpenTelemetry tracing.
  • 🧱 Reference CRUD feature (Articles) wiring all of the above together.
  • 📋 Spec-driven workflow via OpenSpec (openspec/).

🧰 Tech Stack

Area Choice
Runtime Node.js 22+ (uses require() of ESM for @keyv/redis)
Framework NestJS 11
HTTP adapter Fastify (@nestjs/platform-fastify)
Language TypeScript (strict)
Database PostgreSQL via TypeORM (@nestjs/typeorm + pg)
Migrations / Seeds TypeORM CLI (entity-first) + in-house seeder runner
Cache @nestjs/cache-manager + Redis (@keyv/redis), in-memory fallback
Queues BullMQ (@nestjs/bullmq) on a dedicated Redis connection
Config @nestjs/config + class-validator (fail-fast env validation)
Validation / mapping class-validator + class-transformer
Tooling ESLint (flat config), Prettier, Jest + Supertest

🚀 Quick Start

Prerequisites: Node.js 22+, plus PostgreSQL and (optionally) Redis. The fastest way to get the datastores is Docker:

# PostgreSQL
docker run -d --name nest-pg \
  -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=nest_boilerplate \
  -p 5432:5432 postgres:16-alpine

# Redis (optional in dev — cache falls back to in-memory)
docker run -d --name nest-redis -p 6379:6379 redis:7-alpine

Then, from the project root:

# 1. install dependencies
npm install

# 2. create your env file (defaults already point at localhost)
cp .env.example .env

# 3. create the schema and seed example data
npm run migrate
npm run db:seed

# 4. start in watch mode
npm run start:dev

Verify it's up:

curl http://localhost:3000/articles | jq
Prefer a single docker-compose.yml?
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: nest_boilerplate
    ports: ['5432:5432']
  redis:
    image: redis:7-alpine
    ports: ['6379:6379']

Run with docker compose up -d.


⚙️ Configuration

Configuration is entirely env-driven and validated at boot — a missing or invalid variable aborts startup with a message naming the offending variable. Copy .env.example to .env and adjust.

Full environment variable reference

Application

Variable Default Notes
NODE_ENV development Also gates migrate:fresh (only local/development)
PORT 3000 HTTP port
SERVICE_NAME nest-boilerplate Log prefix / app name

Database (required)

Variable Default Notes
DB_HOST — required
DB_PORT 5432
DB_USERNAME — required
DB_PASSWORD — required
DB_DATABASE — required
DB_POOL_MAX 10 Max pool size
DB_POOL_MIN 1 Min idle connections
DB_POOL_IDLE 10000 Idle timeout (ms)
DB_POOL_CONN_TIMEOUT 10000 Acquire timeout (ms)

Database — forward-compatible (default off)

Variable Default Notes
DB_REPLICATION_ENABLED false Master/slave read replicas
DB_MASTER_HOST / DB_SLAVE_HOSTS — Required when replication is on
DB_PGBOUNCER false Transaction-pooling-safe settings
DB_DIRECT_URL — Direct URL so migrations bypass PgBouncer

Cache (Redis via @keyv/redis)

Variable Default Notes
CACHE_REDIS_ENABLED false false → in-memory (no Redis needed)
CACHE_REDIS_HOST — Required when enabled
CACHE_REDIS_PORT 6379
CACHE_REDIS_PASSWORD — Optional
CACHE_REDIS_DB 0
CACHE_TTL 60000 Default entry TTL (ms)

BullMQ (its own Redis, separate from the cache)

Variable Default Notes
BULLMQ_REDIS_HOST — required
BULLMQ_REDIS_PORT 6379
BULLMQ_REDIS_PASSWORD — Optional
BULLMQ_REDIS_DB 0
BULLMQ_PREFIX bull Key prefix

🗂️ Project Structure

src/
  main.ts                 # bootstrap: Fastify + global ValidationPipe + dotenv
  app.module.ts           # wires the global modules below
  database/               # DatabaseModule, DataSource, migrations/, seeders/
  cache/                  # global CacheModule (Redis / in-memory)
  bullmq/                 # global BullMQ root connection
  articles/               # EXAMPLE CRUD feature (reference — safe to delete)
openspec/
  specs/                  # source-of-truth capability specs
  changes/                # in-flight & archived change proposals

🗄️ Database

PostgreSQL via TypeORM, initialized by the global DatabaseModule (src/database/). synchronize is disabled — schema changes go through migrations only.

Migrations (entity-first)

Define or change an entity, then generate a migration by diffing against the DB:

Command What it does
npm run migrate:generate -- src/database/migrations/<Name> Generate from entity diff
npm run migrate:create -- src/database/migrations/<Name> Create an empty migration
npm run migrate Apply pending migrations
npm run migrate:revert Revert the last applied migration
npm run migrate:show Show applied / pending status
npm run migrate:fresh ⚠️ Drop all tables + re-run (only in local/development)

Entities keep camelCase properties mapped to snake_case columns explicitly: @Column({ name: 'created_at' }) createdAt: Date;

Seeding

Seeders live in src/database/seeders/. Implement the Seeder contract, register your seeder in DatabaseSeeder (runs in order), then:

npm run db:seed

⚡ Cache & Queues

The cache and BullMQ each use their own, separate Redis connection — they must not share one, because BullMQ uses blocking commands and requires maxRetriesPerRequest: null. Both are global modules configured from validated env.

Cache (src/cache/)BullMQ (src/bullmq/)

@nestjs/cache-manager + Redis (@keyv/redis), with an in-memory fallback.

// inject anywhere
@Inject(CACHE_MANAGER)
private readonly cache: Cache;

Provides the BullMQ root connection only. Register queues per feature:

BullModule.registerQueue({ name: 'emails' });

📡 Observability

Three pillars, unified by a correlation id so a log line links to its trace:

  • Logs → Loki. Structured JSON via nestjs-pino, always on, written to stdout (a shipper like Grafana Alloy forwards them to Loki — the app never talks to Loki directly). Every request's logs share a correlation id; when tracing is on, that id is the OpenTelemetry trace_id.
  • Metrics → Prometheus. A /metrics endpoint (default process metrics + an HTTP request-duration histogram). Off by default — enable with METRICS_ENABLED=true.
  • Traces → Tempo. OpenTelemetry auto-instrumentation (HTTP/Fastify, pg, ioredis) exported via OTLP. Off by default — enable with OTEL_ENABLED=true and set OTEL_EXPORTER_OTLP_ENDPOINT. The SDK is initialized before the app loads; when disabled it never starts (zero overhead).
Variable Default Notes
LOG_LEVEL info fatal|error|warn|info|debug|trace|silent
METRICS_ENABLED false Exposes GET /metrics when true
OTEL_ENABLED false Starts OpenTelemetry tracing when true
OTEL_SERVICE_NAME nest-boilerplate Service name on traces/logs
OTEL_EXPORTER_OTLP_ENDPOINT — Required when OTEL_ENABLED=true

Local stack (Grafana + Loki + Prometheus + Tempo)

A ready-to-run stack lives in deploy/observability/:

docker compose -f deploy/observability/docker-compose.observability.yml up -d

# then run the app with the pillars on:
METRICS_ENABLED=true OTEL_ENABLED=true \
  OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 npm run start:dev

Open Grafana at http://localhost:3001 (anonymous admin) — Loki, Prometheus, and Tempo datasources are pre-provisioned, and a log's trace_id links straight to its trace in Tempo. For local use only.


📦 Example: Articles CRUD

src/articles/ is a complete, self-contained reference feature — copy it as a template, or delete it when forking (remove src/articles/, the ArticleSeeder and its line in DatabaseSeeder, and the CreateArticles migration/table).

Endpoints

Method Path Description
POST /articles Create an article
GET /articles?page=1&limit=20 Paginated list ({ data, meta })
GET /articles/:id Get one (read-through cached)
PATCH /articles/:id Partial update (evicts cache)
DELETE /articles/:id Delete (evicts cache)

Try it

# create
curl -X POST http://localhost:3000/articles \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello","slug":"hello","content":"First post","published":true}'

# list (paginated)
curl 'http://localhost:3000/articles?page=1&limit=10'

# get one (cached for 30s; evicted on update/delete)
curl http://localhost:3000/articles/<id>
Example responses

GET /articles/:id

{
  "id": "1aa04c36-07e7-4115-936b-f71ad0d32b5a",
  "title": "Hello",
  "slug": "hello",
  "content": "First post",
  "published": true,
  "publishedAt": null,
  "createdAt": "2026-07-01T07:07:49.339Z",
  "updatedAt": "2026-07-01T07:07:49.339Z"
}

GET /articles (paginated envelope)

{
  "data": [ /* ArticleResponseDto[] */ ],
  "meta": { "total": 3, "page": 1, "limit": 10, "pageCount": 1 }
}

Posting an unknown field is rejected by the strict ValidationPipe:

{ "statusCode": 400, "message": ["property isAdmin should not exist"], "error": "Bad Request" }

What it demonstrates: entity → generated migration, a seeder, class-validator DTOs, UpdateArticleDto via PartialType, pagination, response shaping with plainToInstance (ArticleResponseDto), 404 handling, and read-through detail caching with eviction on write.


🧪 Testing

npm run test        # unit tests
npm run test:e2e    # end-to-end tests
npm run test:cov    # coverage

📐 Conventions

  • Fastify only — no Express-specific code or middleware.
  • Official packages first — prefer an @nestjs/* package when one exists.
  • Naming — class members are camelCase; entity columns are snake_case, mapped explicitly (@Column({ name: 'snake_case' })).
  • Validation & mapping — validate input with class-validator; shape responses with a dedicated response DTO via class-transformer (plainToInstance), never by returning the entity directly.

Full details live in CLAUDE.md and openspec/config.yaml.


🧭 Spec-Driven Development

Changes are planned as OpenSpec artifacts (proposal → specs → design → tasks) before implementation. Current capabilities live in openspec/specs/; completed changes are archived under openspec/changes/archive/. See CLAUDE.md for the conventions AI assistants follow.


📜 npm Scripts

Script Purpose
start / start:dev / start:prod Run the app (once / watch / compiled)
build Compile to dist/
lint / format ESLint (--fix) / Prettier
test / test:e2e / test:cov Unit / e2e / coverage
migrate / migrate:generate / migrate:create Apply / generate / scaffold migrations
migrate:revert / migrate:show / migrate:fresh Revert / status / rebuild
db:seed Run the database seeders

🚢 Deployment

Review the NestJS deployment docs. Set NODE_ENV appropriately (note migrate:fresh is refused outside local/development), run npm run migrate on release, and point the cache and BullMQ Redis connections at your managed instances (they can be separate).


📄 License

MIT © Reza. Built with NestJS.

About

A batteries-included NestJS 11 starter (Fastify, PostgreSQL/TypeORM, Redis, BullMQ) built to be forked into new services.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages