NestJS vs Express: when to choose each in 2026
3 July 2026 · SOM-OS
The question keeps appearing in every project: NestJS or Express? In 2026, the answer is not “it depends” out of laziness — it is “it depends” because the two frameworks solve different problems. Express is a minimalist HTTP library. NestJS is an opinionated framework with class-based architecture, dependency injection and a module ecosystem. Choosing poorly is expensive: refactoring a monolithic Express backend into something maintainable takes weeks.
This comparison includes real code, measured benchmarks and a decision matrix. Save it as a reference. For more context on our stack, see isolated apps vs systems.
Philosophy: minimalism vs convention
Express gives you app.get(), app.use() and little else. You decide the structure: where controllers go, how validation works, how dependencies are injected. It is absolute freedom — and the source of most unmaintainable backends that reach us. Each team structures Express its own way; as it grows, the chaos grows.
NestJS imposes architecture: controllers, services, modules, pipes, guards and interceptors. If you come from Spring or Angular, it feels familiar. The initial learning curve is steeper, but convention scales. A NestJS project with 50 modules can be navigated. An Express project with 50 loose files cannot.
The same endpoint in both
To show the difference, here is a POST /users endpoint with validation in each.
Express 5:
import express from 'express';
const app = express();
app.use(express.json());
app.post('/users', async (req, res) => {
const { email, name } = req.body;
if (!email || !email.includes('@'))
return res.status(400).json({ error: 'Invalid email' });
if (!name || name.length < 2)
return res.status(400).json({ error: 'Name is required' });
const user = await db.user.create({ email, name });
res.status(201).json(user);
});
app.listen(3000);
It works. But validation is manual, req.body has no typing and there is no separation of layers. Add 20 more endpoints and this becomes a ball of mud.
NestJS 11:
import { Body, Controller, Post } from '@nestjs/common';
import { IsEmail, MinLength } from 'class-validator';
class CreateUserDto {
@IsEmail() email: string;
@MinLength(2) name: string;
}
@Controller('users')
export class UsersController {
constructor(private readonly users: UsersService) {}
@Post()
create(@Body() dto: CreateUserDto) {
return this.users.create(dto);
}
}
class-validator handles validation through a global ValidationPipe. The DTO defines the contract. The controller simply delegates to the service. Remove the controller and the service remains testable. That separation keeps a project navigable with 200 endpoints.
Performance: real benchmarks
In mid-2026, we measured with autocannon -c 100 -d 30 on Ubuntu 24.04 with Node 22 LTS. Hello-world JSON in each framework:
- Express 5: ~38,000 req/s, P99 latency 3.2ms.
- NestJS 11 (Express adapter): ~34,000 req/s, P99 4.1ms.
- NestJS 11 (Fastify adapter): ~52,000 req/s, P99 2.8ms.
- Fastify 5 (bare): ~63,000 req/s, P99 2.1ms.
NestJS’s overhead over Express is ~10% in hello-world. With the Fastify adapter, NestJS outperforms bare Express. In real endpoints with a database, framework overhead disappears into database latency noise. If you choose Express for 4,000 req/s in hello-world, you are optimising the wrong thing.
A production backend’s bottleneck is almost never the framework. It is the database, external calls and queues. NestJS does not slow you down. It organises you.
Dependency injection
NestJS includes a native DI container. Register a provider in a module and inject it into any service in that module:
@Module({
providers: [UsersService, PrismaService],
controllers: [UsersController],
})
export class UsersModule {}
In Express, you must assemble your own DI: typedi, awilix or passing dependencies manually. It works, but it is homemade. NestJS provides it out of the box, with module scoping and straightforward testing — mock the provider and you are done.
Testing
NestJS shines here. The testing module mocks dependencies without friction:
const moduleRef = await Test.createTestingModule({
controllers: [UsersController],
providers: [
UsersService,
{ provide: PrismaService, useValue: mockPrisma },
],
}).compile();
const ctrl = moduleRef.get(UsersController);
expect(await ctrl.create({ email: 'a@b.c', name: 'Ana' }))
.toEqual({ id: 1, email: 'a@b.c', name: 'Ana' });
In Express, use supertest + manual mocks. It works, but requires discipline. The problem is not that Express cannot be tested — it is that nothing forces you to do it well.
Decision matrix
This is what we put on the table when a client asks which stack to use:
- Use Express if: it is a weekend prototype, a simple webhook, an API with fewer than 10 endpoints and no business logic, or you already have an Express foundation and migration is not worthwhile.
- Use NestJS if: the backend will grow beyond 15 endpoints, business logic spans multiple services, you need queues (BullMQ), websockets or AI agents requiring orchestration.
- Use NestJS + Fastify if: you need maximum performance and have already worked through NestJS’s learning curve. The adapter is one line.
- Use neither if: you need pure real-time with thousands of connections — use Bun + Hono or Go/Rust directly. Node is not always the answer.
The real cost of choosing poorly
We have inherited Express projects with 8,000 lines, no tests, no layer separation and manual validation in every route. Refactoring to modular architecture took three weeks. If it had started in NestJS, that refactor would not have been necessary — convention would have maintained order from day one.
The cost of Express is not on day 1. It is on day 400, when the team has grown, five people edit the same file and nobody knows where the validation logic lives for the endpoint failing in production at 2am.
That is why SOM-OS’s default stack is NestJS 11 + PostgreSQL + Redis + BullMQ + LangChain. Not because Express is bad — because the systems we build need the architecture NestJS imposes. To understand why that architecture matters before writing a line of code, read about the Chaos Map and our isolated apps vs systems approach.
Conclusion
Express is an excellent tool for what it was designed to do: minimalist HTTP. In 2026, it remains valid for small projects and prototypes. NestJS does not replace it — it complements it. As a project grows, NestJS saves you the refactor Express will sooner or later require.
Our rule: if the project lives longer than 6 months or has more than 2 developers, NestJS. If it is an experiment deleted on Monday, Express and move on. The difference is maintainability, not performance. Maintainability costs real money, not the 4,000 req/s you lose in hello-world.
The framework does not make code good. But a framework with convention makes bad code harder to write. In the long term, that is worth more than any benchmark.