Every new project starts the same way.
Open terminal. mkdir new-project. And then... 2 days of configuration before writing a single line of business logic.
After 8 years building APIs professionally — across startups and companies — I realized I was solving the same problems every single time:
- TypeScript strict mode setup that actually works
- JWT auth with refresh token rotation
- A folder structure that doesn't fall apart at scale
- Docker that works the same on every machine
- CI/CD that doesn't take half a day to configure
So I stopped. And I packaged everything into a boilerplate I actually use in production.
What the stack looks like
After trying many combinations, this is what I landed on:
Fastify over Express — it's faster, has better TypeScript support out of the box, and the plugin system is genuinely good once you understand it.
Prisma over raw SQL or other ORMs — the DX is excellent and the generated types save hours of manual typing.
Zod for validation — schema-first validation that integrates cleanly with TypeScript. No more runtime surprises.
Vitest over Jest — faster, same API, zero config with TypeScript.
The folder structure
This is the part most boilerplates get wrong. Everything dumped in a flat src/ folder doesn't scale.
src/
config/ # Zod-validated env vars, constants
modules/ # Feature modules
auth/ # routes, controller, service, schema
users/ # routes, controller, service, schema
middlewares/ # error handler, auth guard, rate limiter
plugins/ # Fastify plugins (Prisma, JWT)
utils/ # AppError, response helpers, logger
app.ts
server.ts
Each module owns its routes, controller, service and validation schema. Adding a new feature means adding a new folder — nothing else changes.
Auth that actually works in production
Most boilerplates ship with basic JWT. This one has:
- Access token (short-lived) + refresh token (long-lived)
- Refresh tokens stored in DB and rotated on every use
- Role-based access control:
authenticate()andauthorize(ROLES.ADMIN)guards - Passwords hashed with Node.js native
scrypt— no bcrypt dependency
// Protecting a route is two lines
fastify.get(
'/admin/users',
{ preHandler: [authenticate, authorize(ROLES.ADMIN)] },
usersController.findAll
)
Error handling that doesn't leak internals
Centralized error handler with a custom AppError class:
// Throw anywhere in your code
throw new AppError('User not found', 404)
// Consistent response format everywhere
// { success: false, message: 'User not found', statusCode: 404 }
Zod validation errors are also caught and formatted automatically — no more unhandled validation crashes.
From zero to running API in 4 commands
cp .env.example .env
docker compose up db -d
npx prisma migrate dev
npm run dev
That's it. PostgreSQL running in Docker, migrations applied, API live on localhost.
CI/CD included
GitHub Actions pipeline with 4 stages:
- Lint — ESLint + Prettier
- Test — Vitest + Supertest integration tests
- Build — TypeScript compilation check
- Docker — multi-stage build (builder + alpine runner, non-root user)
Push to main and everything runs automatically.
The result
38 files. Zero any types. Strict TypeScript throughout. Consistent response format. Graceful shutdown. Pino logger with request ID tracing.
Everything I wished existed when I started my first project.
If you want to skip the setup and start with this foundation, I packaged it here: [link to your Gumroad product]
Or just steal the ideas — either way, I hope it saves you 2 days.
Top comments (0)