DEV Community

Cover image for Why I Switched from Express to NestJS in Production (And What 4 Years Taught Me)
Divya Shakti
Divya Shakti

Posted on

Why I Switched from Express to NestJS in Production (And What 4 Years Taught Me)

Every Node.js developer loves Express.

It is minimalistic, unopinionated, and takes less than 10 lines of code to get an HTTP server running. When I started my backend career, Express was my weapon of choice.

But in 2022, as our backend at MedPiper scaled across enterprise healthcare integrations, financial reporting, and dozens of third-party APIs, our Express codebase started showing its scars:

  • Every new engineer structured their route files differently.
  • Route middleware order was fragile — missing a single authMiddleware in one router file silently left an endpoint unprotected.
  • req.user was mutated arbitrarily across 15 different files, making debugging state bugs an absolute nightmare.
  • Unit testing required heavy monkey-patching because there was no clean dependency injection.

That was the turning point. We migrated our core backend to NestJS.

Four years and millions of production requests later, here is an honest breakdown of why we made the switch, what broke, and what we learned.


1. The Express Paradox: Complete Freedom is a Liability at Scale

Express does not have an opinion on:

  • How you organize your folders
  • How you inject database models
  • How you validate incoming JSON payloads
  • How you handle business logic vs HTTP transport

When you are a solo developer building a weekend MVP, this freedom feels like a superpower.

At team scale, complete freedom is technical debt.

In an unopinionated Express codebase, 5 developers will write 5 different architectural patterns. One engineer will use a service layer, another will write SQL queries directly inside route callbacks, and a third will attach custom properties to the global req object.

NestJS solves this with enforced architectural conventions:

  • Controllers handle HTTP requests and return responses.
  • Services (Providers) encapsulate pure business logic.
  • Modules create explicit, isolated dependency boundaries.

When a new engineer joins a NestJS codebase, they know exactly where the database query lives, where the route validation happens, and how modules talk to each other — within 10 minutes of opening the repo.


2. The 3 Superpowers That Made NestJS Irreplaceable

A. First-Class Dependency Injection (Testability)

In Express, mocking a database service during testing usually involves hacking jest.mock() or passing dependencies manually through 5 function layers.

In NestJS, inversion of control is built into the framework:

@Injectable()
export class UserService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly redis: RedisService,
  ) {}

  async getUser(id: string) {
    const cached = await this.redis.get(`user:${id}`);
    if (cached) return cached;
    return this.prisma.user.findUnique({ where: { id } });
  }
}
Enter fullscreen mode Exit fullscreen mode

In your unit tests, you can mock PrismaService or RedisService in 3 lines without touching the database:

const module = await Test.createTestingModule({
  providers: [
    UserService,
    { provide: PrismaService, useValue: mockPrisma },
    { provide: RedisService, useValue: mockRedis },
  ],
}).compile();
Enter fullscreen mode Exit fullscreen mode

B. Strict Request Lifecycle Pipeline

In Express, middleware handles authentication, logging, data validation, and rate limiting all lumped into one concept: (req, res, next).

NestJS separates these into a clear, predictable execution order:

  1. Middleware: Low-level tasks (CORS, body parsing, helmet headers).
  2. Guards: Authentication & RBAC (e.g., @UseGuards(JwtAuthGuard, RolesGuard)).
  3. Interceptors: Pre/post-controller request transformation and timing.
  4. Pipes: Automatic payload validation and type coercion via class-validator.
  5. Filters: Centralized exception handling and RFC 7807 compliant error responses.

This prevents the classic Express vulnerability where an endpoint is accidentally exposed because someone forgot to chain a middleware function.

C. Automatic OpenAPI / Swagger Documentation

In Express, maintaining Swagger documentation usually means manually updating an outdated swagger.yaml file that inevitably drifts away from reality.

In NestJS, your TypeScript DTOs automatically generate an interactive Swagger UI at /api/docs:

export class CreateUserDto {
  @ApiProperty({ example: 'dev@company.com' })
  @IsEmail()
  email: string;

  @ApiProperty({ example: 'P@ssword123!' })
  @MinLength(8)
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

Your documentation is literally your code. It never gets outdated.


3. What We Got Wrong (The NestJS Pitfalls)

Switching to NestJS wasn't without friction. Here are the two biggest mistakes we made in our first year:

  1. Circular Dependency Loops: If Module A imports Module B, and Module B imports Module A, NestJS will throw an unhelpful startup error. We learned to isolate shared models into clean CommonModule or use forwardRef() sparingly.
  2. The Request-Scope Trap: Making a service @Injectable({ scope: Scope.REQUEST }) forces every dependent service into request scope, causing massive memory allocation and latency spikes under high concurrency. We learned to rely on Singletons paired with Node.js AsyncLocalStorage.

4. Should You Still Use Express in 2026?

Yes, for specific use cases:

  • Single-purpose serverless functions (AWS Lambda, Cloudflare Workers).
  • Simple webhook receivers or micro-proxies under 300 lines of code.
  • Lightweight internal CLI tools.

Use NestJS when:

  • You are building a SaaS product, enterprise backend, or scalable REST/GraphQL API.
  • Multiple developers work on the same backend.
  • You care about automated testing, strict TypeScript validation, and long-term maintainability.

🚀 Accelerate Your Next NestJS Project

If you are starting a new NestJS project, setting up JWT auth rotation, Docker, PostgreSQL, Redis, BullMQ queues, and Swagger from scratch easily takes 40+ hours.

To save you that setup time, I packaged our battle-tested production starter kit:

👉 Get the Production NestJS Enterprise Boilerplate ($29)

(Also available on Contra ($29))

Need a quick desk reference for your workstation?

👉 Download the 1-Page Architecture Master Cheatsheet ($5)

Top comments (0)