Skip to content

Fastify 5 application boilerplate based on clean architecture, domain-driven design, CQRS, functional programming, vertical slice architecture for building production-grade applications πŸš€

License

Notifications You must be signed in to change notification settings

marcoturi/fastify-boilerplate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1,088 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Fastify Boilerplate Logo

Biome Commitizen friendly MIT License GitHub Actions Workflow Status GitHub Actions Workflow Status

A production-ready Fastify 5 boilerplate built on Clean Architecture, CQRS, DDD, and functional programming. Designed as a starting point for real-world applications, the architecture is framework-agnostic at its core β€” the patterns and boundaries translate to any language or framework.

Table of Contents

Features

Category Details
Runtime Native TypeScript via Node.js >= 24 type stripping β€” no build step, no transpiler
Framework Fastify 5 with Awilix DI and Pino logging
API REST (TypeBox schemas, Swagger UI) + GraphQL (Mercurius, GraphiQL in dev)
Database Postgres.js client + DBMate migrations & seeds
Security @fastify/helmet, @fastify/under-pressure for back-pressure
Telemetry Vendor-agnostic OpenTelemetry with auto-instrumentation (disabled by default)
Linting Biome β€” single tool for linting, formatting, and import sorting
Architecture dependency-cruiser validates layer boundaries at CI time
Release Husky + Commitlint + Semantic Release
Client types REST (OpenAPI) and GraphQL types auto-generated and published to npm on every release
Testing E2E with Cucumber (Gherkin), unit/integration with node:test, load tests with k6
Docker Production-ready multi-stage Dockerfile (Alpine, non-root, health check) + Docker Compose
AI-Ready AGENTS.md β€” architecture rules and coding conventions for AI assistants

Prerequisites

Tool Notes
Node.js >= 24 β€” required for native TypeScript execution. A .nvmrc is included β€” run fnm use or nvm use
pnpm >= 10 β€” package manager (corepack enable to activate)
Docker Used to run PostgreSQL via Docker Compose. Alternatively, use a local Postgres install

Getting Started

# 1. Scaffold from the template
npx degit marcoturi/fastify-boilerplate my-app
cd my-app

# 2. Install dependencies
pnpm install

# 3. Create your .env file
pnpm create:env          # copies .env.example β†’ .env

# 4. Start PostgreSQL (pick one)
docker compose up postgres -d   # via Docker Compose
# β€” or use a local Postgres and adjust .env values β€”

# 5. Run database migrations
pnpm db:migrate

# 6. Start the dev server (with watch mode and pretty logs)
pnpm start

The server starts at http://localhost:3000 by default. See API Endpoints for what's available.

Available Scripts

Development

Script Description
pnpm start Start dev server with watch mode and pretty-printed logs
pnpm start:prod Start production server (no watch, no pretty-print)
pnpm create:env Copy .env.example to .env (fails if .env already exists)

Code Quality

Script Description
pnpm check Run lint + format check + type check (use this before committing)
pnpm check:fix Same as check but auto-fixes lint and format issues
pnpm format Auto-format all files with Biome
pnpm lint Run Biome linter with auto-fix
pnpm type:check TypeScript type checking (tsc --noEmit)
pnpm deps:validate Validate architecture layer boundaries with dependency-cruiser
pnpm deps:graph Generate a dependency graph SVG in doc/

Testing

Script Description
pnpm test Run unit tests (alias for test:unit)
pnpm test:unit Run unit and integration tests with node:test
pnpm test:coverage Run unit tests with c8 coverage
pnpm test:e2e Run E2E tests with Cucumber (requires running Postgres)

Database

Script Description
pnpm db:migrate Apply pending migrations
pnpm db:create-migration Create a new migration file
pnpm db:seed Run database seeds
pnpm db:create-seed Create a new seed file

Other

Script Description
pnpm generate:types Generate REST and GraphQL client types (requires running server + DB)

Running with Docker

Full stack (app + Postgres)

pnpm create:env       # create .env from .env.example (if not done already)
docker compose up     # builds the app image and starts all services

Standalone image

docker build -t fastify-boilerplate .
docker run -p 3000:3000 --env-file .env -e HOST=0.0.0.0 fastify-boilerplate

Dockerfile highlights

  • Multi-stage build β€” dependencies installed in an isolated stage for optimal layer caching
  • Node Alpine β€” small footprint, native TypeScript execution (no build step)
  • Non-root user β€” runs as an unprivileged fastify user (UID 1001)
  • dumb-init β€” proper PID 1 signal forwarding for graceful shutdown
  • HEALTHCHECK β€” built-in Docker health check against /health every 30 seconds

API Endpoints

Endpoint Description
GET /health Health check (@fastify/under-pressure)
/api/... All REST routes are prefixed with /api (e.g. /api/v1/users)
GET /api-docs Swagger UI (interactive API documentation)
GET /api-docs/json OpenAPI 3.1.0 JSON spec
POST /graphql GraphQL endpoint (Mercurius)
GET /graphql GraphiQL IDE (development only)

Architecture

Architecture Diagram Diagram adapted from Domain-Driven Hexagon

Principles

Project-level:

  • Adaptable complexity β€” the structure scales up or down by adding or removing layers to match the application's actual needs.
  • Future-proofing β€” framework code and business logic are separated. Dependencies are well-established and minimal.
  • Functional programming first β€” composition and factory functions over classes and inheritance.
  • Microservices-ready β€” vertical slices, path aliases, and CQRS make it straightforward to extract a module into its own service later.

Code-level:

  • Framework-agnostic core β€” business logic has no Fastify dependency. Fastify concerns stay in routes.
  • Protocol-agnostic handlers β€” command/query handlers serve REST, GraphQL, gRPC, or CLI equally.
  • Database-agnostic domain β€” SQL stays in repository files. Handlers interact with data through repository ports (interfaces).
  • Inward dependency flow β€” outer layers depend on inner layers, never the reverse: Route β†’ Handler β†’ Domain β†’ Repository.

Based on:

Modules

Each module maps to a domain concept and lives in its own folder under src/modules/. Modules follow the vertical slice architecture β€” everything a feature needs is co-located.

Key rules:

  • No direct imports between modules. Cross-module communication uses the CQRS buses (commands/queries for request-response, events for fire-and-forget).
  • Extractable β€” any module can be pulled into a separate microservice. The CQRS handler boundary becomes the network boundary.
  • If two modules are too "chatty", they probably belong together β€” merge them.

Module Components

Each layer has a single responsibility:

Route β€” handles the HTTP/GraphQL/gRPC request. Validates input, formats the response. No business logic.

All REST routes are prefixed with /api (configured in src/server/index.ts).

Example: find-users.route.ts

Command/Query Handler β€” orchestrates the use case. Receives a command or query, calls domain services and repositories through ports, returns a result. One handler per use case (e.g. CreateUser, FindUsers).

Benefits of the CQRS bus pattern:

  • Middlewares β€” cross-cutting concerns (auth, caching, tracing, rate limiting) plug in between route and handler. Middleware targeting is pattern-based (e.g. users/* for all user commands, users/create for a specific one). See middlewares.ts.
  • Decoupling β€” modules communicate through the bus instead of direct imports, making future extraction to microservices trivial.

Example: find-users.handler.ts

Domain Service β€” pure business logic. Computes properties, enforces invariants, composes entities. No infrastructure dependencies.

Example: user.domain.ts

Repository β€” data access. Converts between domain models and database rows. All SQL lives here. Implements a port (interface) defined alongside it.

Example: user.repository.ts

Guideline: use as many or as few layers as needed. Not every feature requires a domain service β€” simpler CRUD operations can go straight from handler to repository.

Folder Structure

.
β”œβ”€β”€ db/
β”‚   β”œβ”€β”€ migrations/                β†’ SQL migration files (DBMate)
β”‚   └── seeds/                     β†’ SQL seed files (DBMate)
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ <feature>/
β”‚   β”‚   β”œβ”€β”€ <scenario>.feature     β†’ Gherkin E2E scenarios
β”‚   β”‚   └── <scenario>.k6.ts      β†’ k6 load test scripts
β”‚   β”œβ”€β”€ shared/                    β†’ Shared step definitions
β”‚   └── support/                   β†’ Test server, hooks, custom world
β”œβ”€β”€ client/                        β†’ Generated REST + GraphQL client types (npm package)
β”œβ”€β”€ scripts/                       β†’ Type generation scripts
└── src/
    β”œβ”€β”€ instrumentation.ts         β†’ OpenTelemetry setup (loaded via --import)
    β”œβ”€β”€ config/                    β†’ Environment validation (env-schema + TypeBox)
    β”œβ”€β”€ modules/
    β”‚   └── <feature>/
    β”‚   β”‚   β”œβ”€β”€ commands/
    β”‚   β”‚   β”‚   └── <command>/
    β”‚   β”‚   β”‚       β”œβ”€β”€ command.handler.ts         β†’ Command handler
    β”‚   β”‚   β”‚       β”œβ”€β”€ command.route.ts           β†’ REST route
    β”‚   β”‚   β”‚       β”œβ”€β”€ command.resolver.ts        β†’ GraphQL resolver
    β”‚   β”‚   β”‚       β”œβ”€β”€ command.graphql-schema.ts  β†’ GraphQL type definitions
    β”‚   β”‚   β”‚       └── command.schema.ts          β†’ TypeBox request/response schemas
    β”‚   β”‚   β”œβ”€β”€ queries/
    β”‚   β”‚   β”‚   └── <query>/
    β”‚   β”‚   β”‚       β”œβ”€β”€ query.handler.ts           β†’ Query handler
    β”‚   β”‚   β”‚       β”œβ”€β”€ query.route.ts             β†’ REST route
    β”‚   β”‚   β”‚       β”œβ”€β”€ query.resolver.ts          β†’ GraphQL resolver
    β”‚   β”‚   β”‚       β”œβ”€β”€ query.graphql-schema.ts    β†’ GraphQL type definitions
    β”‚   β”‚   β”‚       └── query.schema.ts            β†’ TypeBox request/response schemas
    β”‚   β”‚   β”œβ”€β”€ database/
    β”‚   β”‚   β”‚   β”œβ”€β”€ feature.repository.port.ts     β†’ Repository interface (port)
    β”‚   β”‚   β”‚   └── feature.repository.ts          β†’ Repository implementation (adapter)
    β”‚   β”‚   β”œβ”€β”€ domain/
    β”‚   β”‚   β”‚   β”œβ”€β”€ feature.domain.ts              β†’ Domain service
    β”‚   β”‚   β”‚   β”œβ”€β”€ feature.errors.ts              β†’ Domain-specific errors
    β”‚   β”‚   β”‚   └── feature.types.ts               β†’ Domain types
    β”‚   β”‚   β”œβ”€β”€ dtos/
    β”‚   β”‚   β”‚   β”œβ”€β”€ feature.graphql-schema.ts      β†’ Shared GraphQL schema
    β”‚   β”‚   β”‚   └── feature.response.dto.ts        β†’ Shared response DTO
    β”‚   β”‚   β”œβ”€β”€ index.ts                           β†’ Action creators, DI declarations
    β”‚   β”‚   └── feature.mapper.ts                  β†’ Entity ↔ DB model ↔ DTO mapper
    β”œβ”€β”€ server/
    β”‚   β”œβ”€β”€ index.ts               β†’ Fastify instance setup
    β”‚   └── plugins/               β†’ Fastify plugins (swagger, CORS, error handler, CQRS, etc.)
    └── shared/
        β”œβ”€β”€ cqrs/                  β†’ Command/Query/Event bus, middlewares
        β”œβ”€β”€ db/                    β†’ Postgres connection, transaction helpers, repository base
        β”œβ”€β”€ exceptions/            β†’ Base exception classes
        └── utils/                 β†’ Cross-cutting utilities

OpenTelemetry

The project ships with a vendor-agnostic OpenTelemetry setup in src/instrumentation.ts. It uses the standard OTLP protocol, so it works with any backend (Grafana, Datadog, Honeycomb, Jaeger, etc.) without code changes.

How it works:

  • HTTP + Fastify β€” the SDK registers the ESM loader hook and initialises with instrumentation-http and @fastify/otel. Every request gets a trace span with route, method, status code, and lifecycle hooks.
  • CQRS β€” a tracing middleware in src/shared/cqrs/otel-middleware.ts wraps every command, query, and event in a span. Spans include the action type, bus kind, and correlation ID.
  • Disabled by default (OTEL_SDK_DISABLED=true). When disabled, @opentelemetry/api returns noop implementations β€” zero overhead.

To enable, set the standard OTel environment variables in your .env:

OTEL_SDK_DISABLED=false
OTEL_SERVICE_NAME=fastify-boilerplate
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318   # your OTLP collector

All configuration uses standard OTel environment variables β€” no vendor lock-in.

Testing

Unit and integration tests

Run with node:test. Test files live next to their source files as *.spec.ts.

pnpm test:unit           # run tests
pnpm test:coverage       # run with c8 coverage

E2E tests

Written in Gherkin and executed with Cucumber.js. Scenarios live in tests/<feature>/<scenario>.feature, step definitions in tests/<feature>/<feature>.steps.ts.

# Requires a running Postgres with migrations applied
pnpm test:e2e

The E2E test server is created via buildApp() (in tests/support/server.ts) β€” it boots a full Fastify instance without binding to a port, so tests run fast and don't conflict with a running dev server.

Load tests

k6 scripts live alongside their feature's E2E tests.

Example: create-user.k6.ts

Client Types Package

The release pipeline automatically generates REST (OpenAPI) and GraphQL client types and publishes them as the @marcoturi/fastify-boilerplate npm package. The version is kept in sync with the backend via semantic-release.

Install

pnpm add -D @marcoturi/fastify-boilerplate

Usage

// REST types (generated by openapi-typescript)
import type { paths, components } from '@marcoturi/fastify-boilerplate/rest';

// GraphQL types (generated by graphql-codegen)
import type { User, Query, Mutation } from '@marcoturi/fastify-boilerplate/graphql';

Generate locally

To regenerate the types against a local server (requires running Postgres with migrations applied):

pnpm generate:types

This starts the server, fetches the OpenAPI and GraphQL schemas, writes the type files to client/, and stops the server.

CI/CD Pipeline

The project uses GitHub Actions with two workflows:

release.yml β€” runs on every push to main:

  1. Install dependencies (pnpm install --frozen-lockfile)
  2. Code quality checks (pnpm check)
  3. Unit tests (pnpm test)
  4. E2E tests (pnpm test:e2e) against a Postgres service container
  5. Generate client types (pnpm generate:types)
  6. Publish release via semantic-release (changelog, GitHub release, npm client package)

codeql-analysis.yml β€” runs on pushes and PRs to main:

  • GitHub CodeQL security analysis for JavaScript/TypeScript

AI-Assisted Development

This project ships with an AGENTS.md file β€” a comprehensive guide for AI coding assistants. It documents the architecture, CQRS patterns, coding conventions, and common pitfalls so that tools like Cursor, Claude Code, and GitHub Copilot can generate code that follows the project's established patterns.

AI assistants automatically pick up AGENTS.md and apply the conventions without manual prompting.

Why Biome over ESLint + Prettier?

This project uses Biome as a single tool for linting, formatting, and import sorting:

  • One tool, zero plugins β€” no @typescript-eslint/parser, eslint-config-prettier, eslint-plugin-import, or other ecosystem packages to keep in sync.
  • Fast β€” written in Rust, orders of magnitude faster than ESLint + Prettier. Noticeable in CI and pre-commit hooks.
  • Stable β€” no more breakage from mismatched plugin versions or peer dependency conflicts across the ESLint ecosystem.
  • Mature β€” covers the vast majority of rules that ESLint + typescript-eslint provide, with a growing community and clear roadmap.

Useful Resources

Contributing

Contributions are welcome! This project uses Conventional Commits enforced by Commitlint and Husky.

  1. Fork and clone the repo
  2. Create a branch: git checkout -b your-feature
  3. Make your changes
  4. Run pnpm check to validate lint, format, and types
  5. Run pnpm test (and pnpm test:e2e if your change touches API behavior)
  6. Commit using Conventional Commits format (e.g. feat: add user roles)
  7. Open a Pull Request

License

MIT

About

Fastify 5 application boilerplate based on clean architecture, domain-driven design, CQRS, functional programming, vertical slice architecture for building production-grade applications πŸš€

Topics

Resources

License

Stars

Watchers

Forks

Contributors 3

  •  
  •  
  •