04/20/26

NestJS vs Express: Which to Choose in 2026

Comparing the two most common Node.js backend frameworks, and a third option worth knowing

9 Min Read

Express is the most-used Node.js framework ever written. It's minimal, unopinionated, and has shipped more backends than any other JavaScript tool. NestJS is the heavyweight counterweight: opinionated, TypeScript-first, modeled on Angular, and built for teams that want structure handed to them.

Picking between them comes down to how much framework you want. This guide compares NestJS and Express on the dimensions that actually matter when you're choosing, architecture, TypeScript support, productivity, performance, and ecosystem, with honest tradeoffs and concrete code. We also introduce a third option that's worth knowing about because it solves problems both frameworks leave to you.

TL;DR

  • Pick Express if you want minimal abstractions, maximum flexibility, a familiar API, and you're happy to pick your own validation, ORM, auth, and structure.
  • Pick NestJS if you want an opinionated architecture out of the box, your team knows Angular or enterprise patterns, and you're willing to learn its DI system and module conventions.
  • Consider Encore if you want type-safe APIs, managed infrastructure, and built-in observability without assembling them yourself. More on this below.

Quick Comparison

AspectExpressNestJS
First release20102017
PhilosophyMinimal, unopinionatedOpinionated, Angular-style
TypeScriptOptional (typings needed)First-class
ArchitectureMiddleware chainModules, controllers, providers, DI
ValidationDIY (bring your own)Built-in (class-validator)
TestingDIY setupBuilt-in testing module
Learning curveLowMedium-high
Performance~30k req/sec~28k req/sec (Express adapter)
Community sizeVery largeLarge, growing
Typical useAPIs, middleware, prototypesEnterprise backends, large teams

Architecture

Express

Express is a middleware dispatcher. You register middleware functions, and each request flows through them in order. A route handler is just a function with (req, res, next).

import express from "express";

const app = express();
app.use(express.json());

app.get("/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) return res.status(404).json({ error: "not found" });
  res.json(user);
});

app.listen(3000);

That's the whole app. There's no module system, no DI container, no project structure conventions. You organize files however you want. For small projects this is freeing; for large ones it becomes an archaeology problem.

NestJS

NestJS gives you a strict architecture: modules group related code, controllers handle HTTP, providers hold business logic, and a DI container wires them together.

// users.controller.ts
import { Controller, Get, Param, NotFoundException } from "@nestjs/common";
import { UsersService } from "./users.service";

@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(":id")
  async findOne(@Param("id") id: string) {
    const user = await this.usersService.findOne(id);
    if (!user) throw new NotFoundException();
    return user;
  }
}

// users.service.ts
import { Injectable } from "@nestjs/common";

@Injectable()
export class UsersService {
  async findOne(id: string) {
    return await this.db.users.findById(id);
  }
}

// users.module.ts
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Same endpoint, three files and a module registration. In exchange you get dependency injection, automatic testing setup, consistent project structure across teams, and a convention for every common backend need.

TypeScript Support

Express was written before TypeScript was popular. It has third-party typings (@types/express), but req.body, req.params, and req.query are typed as any by default. You have to add validation middleware (Zod, Joi, class-validator) and manually narrow types in every handler.

NestJS was built TypeScript-first. Validation is built in via class-validator, parameters are typed at the decorator level, and the DI container is fully typed. You get compile-time safety across the whole application.

This matters more than it sounds. In a 50-endpoint Express app, the "what shape is this request body" question gets answered differently by different developers. In NestJS, there's one way to do it.

Productivity

Express is faster for quick starts. You can have a working API in 10 lines. You'll move fast until you need authentication, validation, database access, testing setup, and error handling, at which point you're assembling a framework from npm packages, and every team does it slightly differently.

NestJS is slower to start (more to learn) but more consistent at scale. Everyone writes code the same way. Adding new developers is easier because the conventions are the framework, not tribal knowledge.

A rough heuristic:

  • < 10 endpoints, solo dev: Express wins on velocity.
  • > 30 endpoints, team of 3+: NestJS wins on consistency.
  • In between: toss-up; depends on team preference.

Performance

Both frameworks are fast enough for almost all workloads. On identical hardware running a simple JSON endpoint:

  • Express: ~30k req/sec
  • NestJS (Express adapter): ~28k req/sec
  • NestJS (Fastify adapter): ~45k req/sec

NestJS uses Express as its default HTTP layer, so it inherits Express's performance minus a small DI overhead. Swapping to the Fastify adapter makes NestJS faster than raw Express. In practice, your database and network I/O dominate, framework overhead is rarely the bottleneck.

Ecosystem

Express has the largest middleware ecosystem in JavaScript. Any problem you hit has 5 npm packages already solving it. The downside is quality and maintenance vary wildly.

NestJS has a smaller but more curated ecosystem. Its @nestjs/* packages cover common needs (auth, config, throttling, caching, GraphQL, WebSockets, microservices) and are maintained by the core team. Beyond those, you can usually wrap any Express middleware inside a NestJS app.

Validation

Express has no built-in validation. You install Zod, Joi, class-validator, or similar.

// Express with Zod
import { z } from "zod";

const CreateUser = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

app.post("/users", (req, res) => {
  const parsed = CreateUser.safeParse(req.body);
  if (!parsed.success) return res.status(400).json(parsed.error);
  // ... use parsed.data
});

NestJS has it wired in via class-validator and class-transformer:

// NestJS DTO
import { IsEmail, IsString, MinLength } from "class-validator";

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(1)
  name: string;
}

@Post()
create(@Body() dto: CreateUserDto) {
  return this.usersService.create(dto);
}

The validation happens before your handler runs. Fewer places to forget it.

Testing

Express gives you supertest and a blank page. You write your own test setup, mock your own dependencies, and decide your own conventions.

NestJS ships a @nestjs/testing module that integrates with the DI container. You can override any provider with a mock in a single line:

const module = await Test.createTestingModule({
  controllers: [UsersController],
  providers: [
    {
      provide: UsersService,
      useValue: { findOne: jest.fn().mockResolvedValue({ id: "1" }) },
    },
  ],
}).compile();

For unit tests, NestJS saves real time. For end-to-end tests, both frameworks converge on supertest-style HTTP testing.

When to Use Each

Use Express when:

  • You're building a small API or a prototype
  • You're the only developer and speed to first endpoint matters
  • You already have strong opinions about how to structure a backend
  • You need maximum flexibility to mix and match libraries

Use NestJS when:

  • You're building a long-lived enterprise backend
  • Your team is 3+ developers and consistency matters
  • You want validation, testing, and DI out of the box
  • You have Angular background and want similar patterns on the backend

A Third Option: Encore

Both Express and NestJS stop at the application boundary. They help you write HTTP handlers. Everything outside that (databases, Pub/Sub, cron jobs, deployment, tracing, service-to-service type safety), is left to you. That's fine for small apps and painful for larger systems.

Encore is a newer TypeScript backend framework that takes a different approach: you declare infrastructure as typed objects in your code, and Encore provisions the actual resources (Postgres, Pub/Sub, Cron) on AWS or GCP. The same TypeScript that defines your APIs also defines your infra.

The endpoint that took a controller + service + module in NestJS, or a route handler + custom validation in Express, is one function in Encore:

import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";

// Provisions managed Postgres, locally via Docker, in production on RDS or Cloud SQL.
const db = new SQLDatabase("users", { migrations: "./migrations" });

interface User {
  id: number;
  email: string;
  name: string;
}

export const get = api(
  { method: "GET", path: "/users/:id", expose: true },
  async ({ id }: { id: number }): Promise<User> => {
    return await db.queryRow`SELECT * FROM users WHERE id = ${id}`;
  },
);

There are no decorators, no DTOs, no DI container, and no validation middleware. Types are the contract: requests are validated against them automatically, and Encore generates an OpenAPI spec, typed client SDKs, and distributed traces from the same definitions.

What Encore handles that Express and NestJS don't:

  • Infrastructure: databases, queues, Pub/Sub, cron jobs declared in code and provisioned automatically on AWS or GCP
  • Type-safe cross-service calls: compile-time type safety across microservices (NestJS's ClientProxy.send returns any)
  • Distributed tracing: every request traced across services, DB queries, and message bus, out of the box
  • Local parity: encore run starts the whole system (services, databases, Pub/Sub) locally with one command

Encore has over 11,000 GitHub stars and is used in production by companies including Groupon. It's open source.

Deploy with Encore

Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.

Deploy

When Encore makes sense

  • You're starting a new backend and don't want to assemble a framework
  • You expect to end up with multiple services eventually
  • You want type safety to extend across services, not stop at HTTP boundaries
  • You'd rather not run Terraform / Kubernetes / OpenTelemetry yourself

When to stick with Express or NestJS

  • You're committed to a specific non-AWS/GCP cloud (Encore targets AWS and GCP natively; other clouds work via its Terraform provider)
  • You have deep investment in Express or NestJS patterns you don't want to rewrite
  • You need a specific middleware or plugin that only exists in those ecosystems

Verdict

For a new TypeScript backend in 2026 where you don't have existing code to preserve, the honest ranking by time-to-productive-system is:

  1. Encore: if you're open to a newer framework. You skip the "which ORM, which validator, which auth library, which deployment story" decisions entirely.
  2. NestJS: if you want established patterns and a large ecosystem, and you're happy with the boilerplate.
  3. Express: if you want full control and are willing to assemble everything yourself.

For an existing Express or NestJS codebase, migration only makes sense if you're hitting real pain (type-safety gaps across services, infrastructure drift, observability blind spots). Otherwise staying put is usually the right call.

Getting Started

# Express
npm init -y && npm install express @types/express typescript

# NestJS
npm install -g @nestjs/cli && nest new project-name

# Encore
brew install encoredev/tap/encore
encore app create my-app --example=ts/empty
cd my-app && encore run
Deploy with Encore

Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.

Deploy

Ready to build your next backend?

Encore is the Open Source framework for building robust type-safe distributed systems with declarative infrastructure.