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.
| Aspect | Express | NestJS |
|---|---|---|
| First release | 2010 | 2017 |
| Philosophy | Minimal, unopinionated | Opinionated, Angular-style |
| TypeScript | Optional (typings needed) | First-class |
| Architecture | Middleware chain | Modules, controllers, providers, DI |
| Validation | DIY (bring your own) | Built-in (class-validator) |
| Testing | DIY setup | Built-in testing module |
| Learning curve | Low | Medium-high |
| Performance | ~30k req/sec | ~28k req/sec (Express adapter) |
| Community size | Very large | Large, growing |
| Typical use | APIs, middleware, prototypes | Enterprise backends, large teams |
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 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.
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.
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:
Both frameworks are fast enough for almost all workloads. On identical hardware running a simple JSON endpoint:
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.
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.
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.
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.
Use Express when:
Use NestJS when:
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:
ClientProxy.send returns any)encore run starts the whole system (services, databases, Pub/Sub) locally with one commandEncore has over 11,000 GitHub stars and is used in production by companies including Groupon. It's open source.
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.
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:
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.
# 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
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.
Yes, NestJS uses Express as its default underlying HTTP framework. It wraps Express with an opinionated layer of modules, controllers, and dependency injection, and you can swap the Express adapter for Fastify if you want higher raw throughput.
No, NestJS is not faster than Express on raw request throughput. Because NestJS runs on top of Express by default and adds abstraction layers, it carries a small overhead, so plain Express is marginally faster in benchmarks. The difference is minor for most applications, and switching NestJS to the Fastify adapter narrows the gap.
It depends on how much of the backend you want the framework to handle. If you also want the infrastructure layer managed, Encore is worth a look: you declare the infrastructure your backend needs as typed objects in TypeScript and it provisions the real resources in your own AWS or GCP account. If you only need HTTP handling, Express is the minimal, flexible choice for small services and prototypes, while NestJS fits larger teams that want enforced structure through modules and dependency injection.
Frameworks with strong conventions and infrastructure defined in code work best with AI coding agents, because the agent has fewer open-ended decisions to get wrong. Encore is built for this: services, databases, and Pub/Sub topics are declared as typed objects, so an agent can generate a full backend including its infrastructure without writing Terraform or touching a cloud console, and the type system limits what it can misconfigure. Express leaves those choices open, which gives an agent more room to make inconsistent decisions.
No, you do not need NestJS to write TypeScript backends. Express works with TypeScript once you add the `@types/express` type definitions, though request bodies and params start out typed as `any`, so you add a validator like Zod. Fastify, Hono, and other frameworks offer stronger type inference without NestJS conventions, so the decorator-and-DI style NestJS uses is one option among several.
The main alternatives include Fastify, Koa, Hapi, and Hono, which offer different tradeoffs on performance and API design. Encore is another option that pairs a backend framework with infrastructure provisioning, letting you declare resources such as databases and cron jobs in code and deploy to your own AWS or GCP account.