An open API service indexing awesome lists of open source software.

https://github.com/marcoturi/fastify-boilerplate

Fastify 5 application boilerplate based on clean architecture, domain-driven design, CQRS, functional programming, vertical slice architecture for building production-grade applications 🚀
https://github.com/marcoturi/fastify-boilerplate

agents backend bdd clean-architecture cqrs cucumber ddd docker fastify functional-programming graphql hexagonal-architecture mercurius nodejs onion-architecture opentelemetry typescript vitest

Last synced: 2 days ago
JSON representation

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

Awesome Lists containing this project

README

          

![Fastify Boilerplate Logo](doc/images/fastify_logo.png)

[![Biome](https://img.shields.io/badge/Biome-60a5fa?logo=biome&logoColor=fff)](https://biomejs.dev/) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![MIT License](https://img.shields.io/github/license/marcoturi/fastify-boilerplate)](https://github.com/marcoturi/fastify-boilerplate/blob/main/LICENSE) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/marcoturi/fastify-boilerplate/codeql-analysis.yml) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/marcoturi/fastify-boilerplate/release.yml)

A production-ready [Fastify 5](https://github.com/fastify/fastify) 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](#features)
- [Prerequisites](#prerequisites)
- [Getting Started](#getting-started)
- [Available Scripts](#available-scripts)
- [Running with Docker](#running-with-docker)
- [API Endpoints](#api-endpoints)
- [Architecture](#architecture)
- [Principles](#principles)
- [Modules](#modules)
- [Module Components](#module-components)
- [Folder Structure](#folder-structure)
- [OpenTelemetry](#opentelemetry)
- [Testing](#testing)
- [Client Types Package](#client-types-package)
- [CI/CD Pipeline](#cicd-pipeline)
- [AI-Assisted Development](#ai-assisted-development)
- [Why Biome over ESLint + Prettier?](#why-biome-over-eslint--prettier)
- [Useful Resources](#useful-resources)
- [Contributing](#contributing)
- [License](#license)

## Features

| Category | Details |
|---|---|
| **Runtime** | Native TypeScript via [Node.js >= 24](https://nodejs.org/en/blog/release/v24.0.0) type stripping — no build step, no transpiler |
| **Framework** | [Fastify 5](https://github.com/fastify/fastify) with [Awilix](https://github.com/jeffijoe/awilix) DI and [Pino](https://github.com/pinojs/pino) logging |
| **API** | REST ([TypeBox](https://github.com/sinclairzx81/typebox) schemas, Swagger UI) + GraphQL ([Mercurius](https://github.com/mercurius-js/mercurius), GraphiQL in dev) |
| **Database** | [Postgres.js](https://github.com/porsager/postgres) client + [DBMate](https://github.com/amacneil/dbmate) migrations & seeds |
| **Security** | [@fastify/helmet](https://github.com/fastify/fastify-helmet), [@fastify/under-pressure](https://github.com/fastify/under-pressure) for back-pressure |
| **Telemetry** | Vendor-agnostic [OpenTelemetry](https://opentelemetry.io/) with auto-instrumentation (disabled by default) |
| **Linting** | [Biome](https://biomejs.dev/) — single tool for linting, formatting, and import sorting |
| **Architecture** | [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) validates layer boundaries at CI time |
| **Release** | [Husky](https://github.com/typicode/husky) + [Commitlint](https://commitlint.js.org/) + [Semantic Release](https://github.com/semantic-release/semantic-release) |
| **Client types** | REST (OpenAPI) and GraphQL types [auto-generated and published to npm](#client-types-package) on every release |
| **Testing** | E2E with [Cucumber](https://cucumber.io/docs/installation/javascript/) (Gherkin), unit/integration with `node:test`, load tests with [k6](https://github.com/grafana/k6) |
| **Docker** | Production-ready multi-stage [Dockerfile](Dockerfile) (Alpine, non-root, health check) + [Docker Compose](docker-compose.yml) |
| **AI-Ready** | [AGENTS.md](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

```bash
# 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](#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)

```bash
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

```bash
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](https://github.com/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](https://github.com/mercurius-js/mercurius)) |
| `GET /graphql` | GraphiQL IDE (development only) |

## Architecture

![Architecture Diagram](doc/images/fastify-boilerplate.png)
Diagram adapted from [Domain-Driven Hexagon](https://github.com/Sairyss/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:

- [Domain-Driven Design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design)
- [Hexagonal (Ports and Adapters) Architecture]()
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Onion Architecture](https://herbertograca.com/2017/09/21/onion-architecture/)
- [SOLID Principles](https://en.wikipedia.org/wiki/SOLID)
- [Vertical Slice Architecture](https://www.jimmybogard.com/vertical-slice-architecture/)
- [Common Closure Principle (CCP)](https://en.wikipedia.org/wiki/Common_closure_principle)

### Modules

Each module maps to a domain concept and lives in its own folder under `src/modules/`. Modules follow the [vertical slice architecture](https://www.jimmybogard.com/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](src/modules/user/queries/find-users/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](src/shared/cqrs/middlewares.ts).
- **Decoupling** — modules communicate through the bus instead of direct imports, making future extraction to microservices trivial.

Example: [find-users.handler.ts](src/modules/user/queries/find-users/find-users.handler.ts)

**Domain Service** — pure business logic. Computes properties, enforces invariants, composes entities. No infrastructure dependencies.

Example: [user.domain.ts](src/modules/user/domain/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](src/modules/user/database/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 → Gherkin E2E scenarios
│ │ └── .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/
│ └── /
│ │ ├── commands/
│ │ │ └── /
│ │ │ ├── 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.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](https://opentelemetry.io/) setup in [`src/instrumentation.ts`](src/instrumentation.ts). It uses the standard [OTLP](https://opentelemetry.io/docs/specs/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](https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/esm-support.md) and initialises with `instrumentation-http` and [`@fastify/otel`](https://github.com/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`](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`:

```bash
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](https://opentelemetry.io/docs/languages/sdk-configuration/general/) — no vendor lock-in.

## Testing

### Unit and integration tests

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

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

### E2E tests

Written in [Gherkin](https://cucumber.io/docs/gherkin/) and executed with [Cucumber.js](https://cucumber.io/docs/installation/javascript/). Scenarios live in `tests//.feature`, step definitions in `tests//.steps.ts`.

```bash
# 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](https://github.com/grafana/k6) scripts live alongside their feature's E2E tests.

Example: [create-user.k6.ts](tests/user/create-user/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`](https://www.npmjs.com/package/@marcoturi/fastify-boilerplate) npm package. The version is kept in sync with the backend via semantic-release.

### Install

```bash
pnpm add -D @marcoturi/fastify-boilerplate
```

### Usage

```typescript
// 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):

```bash
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](.github/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](.github/workflows/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`](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](https://cursor.com/), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), and [GitHub Copilot](https://github.com/features/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](https://biomejs.dev/) 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

- [Domain-Driven Hexagon](https://github.com/Sairyss/domain-driven-hexagon) — the primary inspiration for this project's architecture (adapted toward functional programming)
- [react-redux boilerplate](https://github.com/marcoturi/react-redux-boilerplate) — companion frontend boilerplate by the same author

## Contributing

Contributions are welcome! This project uses [Conventional Commits](https://www.conventionalcommits.org/) 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](https://www.conventionalcommits.org/) format (e.g. `feat: add user roles`)
7. Open a Pull Request

## License

[MIT](https://choosealicense.com/licenses/mit/)