A production-ready monorepo template for building lightning-fast, enterprise-grade REST APIs with Fastify. Powered by Turborepo for blazing-fast builds, native Node.js 23+ TypeScript type stripping for zero-overhead development, and battle-tested plugins for authentication, validation, and documentation.
Quick Navigation: π Quick Start β’ π Features β’ π οΈ Development Guide β’ π― Examples
- β‘ Why Use Fastify Forge?
- π Quick Start
- π οΈ What Makes Fastify Forge Special
- π¦ Monorepo Architecture
- π Development Guide
- π― Real-World Examples
- π’ Production Deployment
- π§ͺ Testing & Quality Assurance
- π§ Advanced Configuration
- π€ Contributing
"From idea to production in under 5 minutes" - That's the Fastify Forge promise.
- π₯ Performance First: Fastify is the fastest Node.js framework
- β‘ Native TypeScript: Node.js 23+ type stripping enabled - run TypeScript directly without transpilation overhead
- π¦ Monorepo Architecture: Turborepo-powered workspace for scalable, modular development
- π‘οΈ Enterprise Security: Built-in CORS, Helmet, Rate Limiting, and Authentication with Better Auth
- π Type Safety: Full TypeScript support with runtime validation using TypeBox
- π Auto Documentation: Swagger/OpenAPI docs generated automatically with Scalar UI
- ποΈ Database Ready: PostgreSQL with Drizzle ORM for type-safe database operations
- π³ Docker Ready: Production-ready containerization included
- Startups building MVPs that need to scale
- Enterprise teams requiring robust, maintainable APIs with shared packages
- Full-stack developers who want backend peace of mind
- DevOps engineers seeking deployment simplicity
- Teams needing multiple services in a single repository
Create a new project in seconds:
npx fastify-forge@latest
cd my-api
# Install dependencies
pnpm install
# Start the development server with hot-reload (uses Turborepo)
pnpm dev
That's it! Your API is now running at http://localhost:3000
The development server leverages Node.js 23+ native TypeScript type stripping - no build step needed, just pure hot-reload development.
- β Monorepo Structure: Apps and packages organized with pnpm workspaces
- β Turborepo Orchestration: Lightning-fast builds and task execution
- β
API Documentation: Visit
http://localhost:3000/docs
for beautiful Scalar UI - β
Health Check:
GET /health
endpoint ready to use - β Type Safety: Full TypeScript support with import aliases
- β Authentication: Better Auth integration with session management
- β Database: PostgreSQL with Drizzle ORM and migrations
- β
Shared Packages: Reusable
@workspace/db
,@workspace/env
,@workspace/logger
curl http://localhost:3000/health
# Response: {"status":"ok"}
Fastify Forge leverages Node.js 23+ native TypeScript type stripping, allowing you to run TypeScript files directly without a build step during development:
# No transpilation needed! Just run TypeScript directly
node --watch src/server.ts
This means:
- Instant startup - no compilation wait times
- True hot-reload - see changes immediately
- Simplified debugging - debug TypeScript directly in Node.js
- Reduced tooling complexity - fewer build tools to configure
Built on Turborepo for maximum developer productivity:
# Run tasks across all packages in parallel
pnpm build # Builds all apps and packages with caching
pnpm test # Tests everything in the right order
pnpm lint # Lints all workspaces simultaneously
Benefits:
- Incremental builds - only rebuild what changed
- Task orchestration - run tasks in dependency order automatically
- Remote caching - share build artifacts across your team
- Pipeline optimization - parallel execution where possible
// Built-in security headers, CORS, and rate limiting
// Zero configuration required!
app.register(helmet);
app.register(cors);
app.register(rateLimit, {
max: 100,
timeWindow: '1 minute'
});
// Define your API with full type safety
const UserSchema = Type.Object({
id: Type.String(),
email: Type.String({ format: 'email' }),
name: Type.String({ minLength: 1 })
});
app.post(
'/users',
{
schema: {
body: UserSchema,
response: {
201: UserSchema
}
}
},
async (request, reply) => {
// request.body is fully typed!
const user = await createUser(request.body);
return reply.code(201).send(user);
}
);
Beautiful, interactive API documentation powered by Scalar, generated automatically from your TypeScript schemas.
Fastify Forge uses a modular monorepo structure powered by pnpm workspaces and Turborepo:
fastify-forge/
βββ apps/
β βββ api/ # Main Fastify application
β βββ src/
β β βββ app.ts # Application setup & plugins
β β βββ server.ts # Server entry point (runs with Node.js type stripping)
β β βββ auth.ts # Better Auth configuration
β β βββ decorators/ # Fastify decorators
β β βββ plugins/
β β β βββ external/ # Third-party plugins (CORS, Helmet, etc.)
β β β βββ internal/ # Custom business logic plugins
β β βββ routes/
β β βββ api/v1/ # Versioned API routes
β βββ test/ # API tests
β βββ package.json
β
βββ packages/
β βββ db/ # Shared database package
β β βββ src/
β β β βββ index.ts # Database connection & client
β β β βββ schema.ts # Drizzle ORM schema
β β βββ migrations/ # Database migrations
β β
β βββ env/ # Environment configuration package
β β βββ src/
β β βββ index.ts # TypeBox schema validation
β β
β βββ logger/ # Logging package
β β βββ src/
β β βββ index.ts # Pino logger configuration
β β
β βββ eslint-config/ # Shared ESLint configuration
β βββ typescript-config/ # Shared TypeScript configuration
β
βββ cli/ # Project generator CLI
βββ turbo.json # Turborepo pipeline configuration
βββ pnpm-workspace.yaml # pnpm workspace configuration
βββ docker-compose.yaml # Docker development environment
Shared Packages:
@workspace/db
- Database connection and schema shared across services@workspace/env
- Centralized environment validation@workspace/logger
- Consistent logging across all packages@workspace/eslint-config
- Unified code style rules@workspace/typescript-config
- Shared TypeScript compiler options
Turborepo Tasks:
{
"build": "Compile TypeScript in dependency order",
"dev": "Start development servers with hot-reload",
"test": "Run tests across all packages",
"lint": "Check code quality everywhere",
"clean": "Remove build artifacts from all workspaces"
}
Create a new shared package:
mkdir packages/my-package
cd packages/my-package
pnpm init
Use it in your app:
{
"dependencies": {
"@workspace/my-package": "workspace:*"
}
}
- Node.js 23+ - Required for native TypeScript type stripping support
- pnpm 10+ - Package manager for monorepo management
- PostgreSQL - Database (or use Docker Compose)
Check your Node.js version:
node --version # Should be v23.0.0 or higher
-
Copy the environment template:
cp apps/api/.env.example apps/api/.env
-
Configure your environment variables:
# Database POSTGRES_HOST=localhost POSTGRES_USER=your_user POSTGRES_PASSWORD=your_password POSTGRES_DB=your_database POSTGRES_PORT=5432 # Server HOST=localhost PORT=3000 NODE_ENV=development LOG_LEVEL=info # Better Auth BETTER_AUTH_SECRET=your-secret-key-here BETTER_AUTH_URL=http://localhost:3000
-
Start with Docker Compose (recommended):
docker-compose up -d
-
Run database migrations:
cd packages/db pnpm db:generate # Generate migrations pnpm db:migrate # Apply migrations
-
Start development:
pnpm dev # β Configuration validated automatically on startup # β TypeScript runs natively without transpilation
# Development
pnpm dev # Start all apps in dev mode with hot-reload
pnpm build # Build all apps and packages (cached by Turborepo)
pnpm start # Start all apps in production mode
# Code Quality
pnpm lint # Lint all workspaces
pnpm format # Format code with Prettier
pnpm test # Run all tests
# Maintenance
pnpm clean # Remove all build artifacts and node_modules
pnpm syncpack:list # Check for dependency version mismatches
pnpm syncpack:fix # Fix dependency version mismatches
# From apps/api directory
cd apps/api
pnpm dev # Start API server with hot-reload
pnpm build # Compile TypeScript to dist/
pnpm start # Run production build
pnpm test # Run tests with 100% coverage threshold
pnpm lint # Lint this package only
# From packages/db directory
cd packages/db
pnpm db:generate # Generate migration files from schema
pnpm db:migrate # Apply migrations to database
pnpm db:push # Push schema changes directly (dev only)
pnpm db:studio # Open Drizzle Studio (database GUI)
Create a new route file in apps/api/src/routes/
:
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
const UserSchema = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String({ format: 'email' })
});
const usersRoute: FastifyPluginAsyncTypebox = async (app) => {
app.route({
url: '/users',
method: 'GET',
schema: {
tags: ['Users'],
response: {
200: Type.Array(UserSchema)
}
},
handler: async () => {
return [{ id: '1', name: 'John Doe', email: '[email protected]' }];
}
});
};
export default usersRoute;
Routes are auto-loaded by @fastify/autoload
- just create the file and it's available!
Add custom functionality in apps/api/src/plugins/internal/
:
import type { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
const myPlugin: FastifyPluginAsync = async (app) => {
app.decorate('myUtility', () => {
return 'Hello from my plugin!';
});
};
export default fp(myPlugin, {
name: 'my-plugin'
});
Using Drizzle ORM for type-safe database operations:
import { eq } from 'drizzle-orm';
import { users } from '@workspace/db/schema';
// In your route handler
const getUsers = async () => {
return await app.db.select().from(users);
};
const getUserById = async (id: string) => {
return await app.db.select().from(users).where(eq(users.id, id));
};
const createUser = async (data: NewUser) => {
return await app.db.insert(users).values(data).returning();
};
Import from workspace packages using the @workspace/
alias:
import { logger } from '@workspace/logger';
import env from '@workspace/env';
import { db } from '@workspace/db';
logger.info('Using shared logger');
console.log('Port:', env.PORT);
const users = await db.select().from(users);
Use import aliases for cleaner imports within packages:
// In apps/api/src/**/*.ts
import { buildApp } from '#src/app';
import type { MyType } from '#src/types';
import { getAuthInstance } from '#src/auth';
import { fromNodeHeaders } from 'better-auth';
const protectedRoute: FastifyPluginAsyncTypebox = async (app) => {
// Apply authentication hook to all routes in this plugin
app.addHook('onRequest', async (req, res) => {
const session = await getAuthInstance(app).api.getSession({
headers: fromNodeHeaders(req.headers)
});
if (!session?.user) {
return res.unauthorized('You must be logged in');
}
req.setDecorator('session', session);
});
app.route({
url: '/profile',
method: 'GET',
schema: {
tags: ['User'],
security: [{ cookieAuth: [] }]
},
handler: async (request) => {
// request.session is available after authentication
return { user: request.session.user };
}
});
};
const uploadRoute: FastifyPluginAsyncTypebox = async (app) => {
app.route({
url: '/upload',
method: 'POST',
schema: {
tags: ['Files'],
consumes: ['multipart/form-data']
},
handler: async (request, reply) => {
const data = await request.file();
if (!data) {
return reply.badRequest('No file uploaded');
}
// Process the file...
return { filename: data.filename, size: data.file.readableLength };
}
});
};
import { posts } from '@workspace/db/schema';
const postsRoute: FastifyPluginAsyncTypebox = async (app) => {
app.route({
url: '/posts',
method: 'POST',
schema: {
tags: ['Posts'],
body: Type.Object({
title: Type.String({ minLength: 1 }),
content: Type.String({ minLength: 1 })
}),
response: {
201: Type.Object({
id: Type.String(),
title: Type.String(),
content: Type.String(),
createdAt: Type.String()
})
}
},
handler: async (request, reply) => {
const { title, content } = request.body;
const [post] = await app.db
.insert(posts)
.values({
title,
content,
authorId: request.session.user.id
})
.returning();
return reply.code(201).send(post);
}
});
};
// packages/email/src/index.ts
export class EmailService {
async send(to: string, subject: string, body: string) {
// Implementation
}
}
// apps/api/src/routes/notifications.ts
import { EmailService } from '@workspace/email';
const notificationRoute: FastifyPluginAsyncTypebox = async (app) => {
const emailService = new EmailService();
app.route({
url: '/send-notification',
method: 'POST',
handler: async (request) => {
await emailService.send(
request.body.email,
'Notification',
request.body.message
);
return { success: true };
}
});
};
The monorepo includes production-ready Docker configuration:
# Build the API image
cd apps/api
docker build -t my-api .
# Or use docker-compose for full stack
docker-compose -f docker-compose.prod.yaml up -d
The production build uses compiled TypeScript from dist/
for optimal performance.
Enable remote caching to speed up builds in CI/CD:
# Link to Vercel for remote caching
npx turbo login
npx turbo link
Now all builds are cached and shared across your team and CI pipeline.
# Build everything with Turborepo caching
pnpm build
# Only changed packages will rebuild
pnpm build --filter=api # Build only the API app
Vercel (API Routes):
pnpm build
vercel deploy
Docker on AWS/GCP/Azure:
- Use multi-stage Dockerfile in
apps/api/
- Configure environment variables
- Set health check endpoint:
/health
- Enable container orchestration (ECS, Cloud Run, AKS)
Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastify-api
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: my-api:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
livenessProbe:
httpGet:
path: /health
port: 3000
- Node.js 23+ installed on production servers
- Environment variables configured (use secrets management)
- Database migrations applied
- SSL/TLS certificates in place
- Rate limiting tuned for production load
- Monitoring and logging configured (APM tools)
- Health checks responding on
/health
- Load balancer configured with sticky sessions (for auth)
- Turborepo remote cache enabled for faster builds
- Database connection pooling configured
# Run all tests across the monorepo
pnpm test
# Run tests for a specific package
pnpm --filter api test
# Run tests with coverage (100% threshold)
cd apps/api
pnpm test:lcov
# Run linting across all packages
pnpm lint
Tests are co-located with source code using Node.js native test runner:
apps/api/
βββ src/
β βββ routes/
β βββ users.ts
βββ test/
βββ routes/
βββ users.test.ts
Example test:
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { buildApp } from '#src/app';
describe('Users API', () => {
it('should return users list', async () => {
const app = await buildApp();
const response = await app.inject({
method: 'GET',
url: '/api/v1/users'
});
assert.strictEqual(response.statusCode, 200);
await app.close();
});
});
Fastify Forge enforces 100% test coverage using c8:
{
"scripts": {
"test": "c8 --100 node --test test/**/*.test.ts"
}
}
Coverage report is generated in coverage/
directory.
- ESLint: Catch code issues with Neostandard presets
- Prettier: Consistent code formatting
- TypeScript: Type checking with
tsc --noEmit
- Husky: Pre-commit hooks for quality gates
- lint-staged: Only lint changed files
- Commitlint: Conventional commit messages
Example GitHub Actions workflow:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '23'
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build # Turborepo cache works in CI
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test
Customize build pipeline in turbo.json
:
{
"tasks": {
"build": {
"dependsOn": ["^build"], // Build dependencies first
"outputs": ["dist/**"], // Cache these outputs
"inputs": ["src/**", "package.json"]
},
"dev": {
"cache": false, // Don't cache dev server
"persistent": true // Keep running
}
}
}
Shared TypeScript config in packages/typescript-config/
:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Extend it in your packages:
{
"extends": "@workspace/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
}
}
Extend packages/env/src/index.ts
:
import { Type, Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';
const EnvSchema = Type.Object({
// Existing variables...
REDIS_URL: Type.String(),
SMTP_HOST: Type.String(),
JWT_SECRET: Type.String({ minLength: 32 }),
API_RATE_LIMIT: Type.Number({ default: 100 })
});
export type Env = Static<typeof EnvSchema>;
const env = Value.Decode(EnvSchema, process.env);
export default env;
Modify plugin settings in apps/api/src/app.ts
:
// Custom rate limiting per environment
await app.register(rateLimit, {
max: env.NODE_ENV === 'production' ? 100 : 1000,
timeWindow: '1 minute',
keyGenerator: (request) => request.ip,
errorResponseBuilder: (req, context) => {
return {
statusCode: 429,
error: 'Too Many Requests',
message: `Rate limit exceeded, retry in ${context.after}`
};
}
});
Customize logger in packages/logger/src/index.ts
:
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV === 'development'
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname'
}
}
: undefined,
// Production: JSON logs for log aggregation
timestamp: () => `,"time":"${new Date().toISOString()}"`
});
Configure in packages/db/src/index.ts
:
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import env from '@workspace/env';
const pool = new Pool({
host: env.POSTGRES_HOST,
port: env.POSTGRES_PORT,
user: env.POSTGRES_USER,
password: env.POSTGRES_PASSWORD,
database: env.POSTGRES_DB,
max: env.NODE_ENV === 'production' ? 20 : 10, // Connection pool size
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
export const db = drizzle(pool);
Manage shared dependency versions in pnpm-workspace.yaml
:
catalog:
fastify: ^5.5.0
typescript: ^5.9.2
drizzle-orm: ^0.44.5
packages:
- "apps/*"
- "packages/*"
Then use in package.json:
{
"dependencies": {
"fastify": "catalog:"
}
}
We welcome contributions! Here's how you can help:
- Check existing issues first
- Provide detailed reproduction steps
- Include environment information (Node.js version, OS, etc.)
- Share relevant logs or error messages
- Open an issue with the
enhancement
label - Describe the use case and benefits
- Consider implementation approaches
- Discuss how it fits the monorepo architecture
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature
- Make your changes following the code style
- Run tests:
pnpm test
- Run linting:
pnpm lint
- Commit with conventional commits:
git commit -m 'feat: add amazing feature'
- Push to branch:
git push origin feature/amazing-feature
- Open a Pull Request with clear description
# Clone the repo
git clone https://github.com/flaviodelgrosso/fastify-forge.git
cd fastify-forge
# Install dependencies (monorepo)
pnpm install
# Start development (all apps and packages)
pnpm dev
# Run tests
pnpm test
# Build everything
pnpm build
We follow Conventional Commits:
feat:
- New featurefix:
- Bug fixdocs:
- Documentation changesstyle:
- Code style changes (formatting, etc.)refactor:
- Code refactoringtest:
- Adding or updating testschore:
- Maintenance tasks
All new features must include tests:
import { describe, it } from 'node:test';
import assert from 'node:assert';
describe('My Feature', () => {
it('should work correctly', () => {
assert.strictEqual(1 + 1, 2);
});
});
This project is licensed under the MIT License.
- Fastify Team for the amazing framework
- Turborepo for monorepo orchestration
- TypeBox for type-safe schemas
- Drizzle Team for the excellent ORM
- Better Auth for modern authentication
- All the contributors who make this project better
- ποΈ Monorepo Architecture: Refactored to Turborepo-powered monorepo with pnpm workspaces
- β‘ Native TypeScript: Node.js 23+ type stripping support - run TypeScript without build steps
- π¦ Shared Packages: Modular architecture with
@workspace/db
,@workspace/env
,@workspace/logger
- π Better Auth: Upgraded to Better Auth for modern session-based authentication
- π Scalar UI: Beautiful API documentation with Scalar instead of Swagger UI
- ποΈ Enhanced Database: Full Drizzle ORM integration with migrations and schema management
- β‘ Turborepo: Lightning-fast builds with intelligent caching and task orchestration
If upgrading from v1.x, key changes include:
- Project structure moved to
apps/api/
instead of root - Shared packages available in
packages/
- Scripts now use Turborepo (e.g.,
pnpm build
runs across all packages) - Node.js 23+ required for type stripping support
- Better Auth replaces previous auth implementation
Ready to forge your next API? π₯
npx fastify-forge@latest
Built with β€οΈ using Node.js 23+, Turborepo, and Fastify
Happy coding! π