DEV Community

Cover image for I stopped setting up TypeScript APIs from scratch — here's what I do instead
Alex Gonzalez
Alex Gonzalez

Posted on

I stopped setting up TypeScript APIs from scratch — here's what I do instead

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
Enter fullscreen mode Exit fullscreen mode

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() and authorize(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
)
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That's it. PostgreSQL running in Docker, migrations applied, API live on localhost.

CI/CD included

GitHub Actions pipeline with 4 stages:

  1. Lint — ESLint + Prettier
  2. Test — Vitest + Supertest integration tests
  3. Build — TypeScript compilation check
  4. 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)