Engineering · 7 min read

NestJS vs Express: Which Node.js Framework for Your API?

NestJS vs Express for your Node.js API: architecture, TypeScript, performance, testing, GraphQL and microservices compared, with code and team-size advice.

Express and NestJS are the two names that come up most when a team plans a Node.js API. They are not really rivals: NestJS runs on top of Express by default. The choice is between a minimal library that leaves architecture to you and an opinionated framework that decides most of it up front.

In short: Choose Express for small services, prototypes and teams that want full control with minimal abstraction. Choose NestJS for long-lived APIs, larger teams and products that need consistent structure, dependency injection, built-in testing patterns, GraphQL or microservices. Performance is rarely the deciding factor, and NestJS can switch to Fastify if you need more throughput.

Minimal vs opinionated

Express.js is a small HTTP framework. It gives you routing, middleware and request and response helpers. Everything else, from folder structure and validation to configuration, logging and database access, is your decision. That freedom is why Express powers a huge share of Node.js backends, and also why two Express codebases can look nothing alike.

NestJS is a full application framework inspired by Angular. It organizes code into modules, controllers and providers, wires them together with dependency injection and uses TypeScript decorators to describe routes, validation and permissions. Under the hood it uses an HTTP adapter, Express by default or Fastify if you choose, so you keep access to the Express middleware ecosystem.

Where they stand in 2026

  • Express 5 is current. Version 5.0 arrived in September 2024, and since version 5.1 in March 2025 it has been the default release on npm. It requires Node.js 18 or newer, passes rejected promises from async handlers to your error middleware automatically and uses a stricter route-matching syntax.
  • NestJS 11, released in January 2025, made Express 5 its default HTTP adapter.
  • NestJS 12, released in mid-2026, moved its packages to ES modules (CommonJS applications still work), added Standard Schema support so you can validate requests with libraries such as Zod, Valibot or ArkType, and defaults new ES module projects to Vitest for testing.

The same endpoint in Express and NestJS

Here is a single "get order by ID" endpoint in each.

Express

// server.ts
import express from 'express'
import { findOrder } from './orders.js'

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

app.get('/orders/:id', async (req, res) => {
  // In Express 5, a rejected promise here reaches the error handler
  const order = await findOrder(req.params.id)
  if (!order) return res.status(404).json({ message: 'Order not found' })
  res.json(order)
})

app.listen(3000)

Short and readable. As the API grows, though, you decide where services live, how they get their dependencies and how errors and validation are handled consistently.

NestJS

// orders.controller.ts
import { Controller, Get, Module, NotFoundException, Param } from '@nestjs/common'
import { OrdersService } from './orders.service'

@Controller('orders')
export class OrdersController {
  constructor(private readonly orders: OrdersService) {}

  @Get(':id')
  async findOne(@Param('id') id: string) {
    const order = await this.orders.findById(id)
    if (!order) throw new NotFoundException('Order not found')
    return order
  }
}

@Module({ controllers: [OrdersController], providers: [OrdersService] })
export class OrdersModule {}

More ceremony for one endpoint. The payoff is that every feature in the codebase follows this same shape, the service is injected rather than imported (which makes testing easy) and exceptions become consistent HTTP responses without extra code.

Architecture and TypeScript

NestJS's main value is structure that scales with the team:

  • Modules group a feature's controllers and services, and make dependencies between features explicit.
  • Dependency injection creates and shares services for you, and lets you swap implementations in tests or per environment.
  • Decorators describe routes, parameters, guards (authorization), interceptors (logging, caching, response shaping) and pipes (validation and transformation).
  • TypeScript first. NestJS is written in TypeScript and its CLI generates typed modules, controllers and services.

Express works fine with TypeScript too, with type definitions available for it and most middleware. You just assemble the architecture yourself, often with a router-per-feature pattern, a validation library and a small dependency container if you want one. Experienced developers do this well; mixed-seniority teams often end up with several styles in one codebase.

Performance: Express, NestJS and Fastify

In synthetic "hello world" benchmarks, plain Express is a little faster than NestJS on Express, because NestJS adds a thin layer of routing and dependency resolution. Both are slower in those tests than Fastify, a Node.js framework designed for throughput.

NestJS can run on Fastify by switching to the @nestjs/platform-fastify adapter, usually with few code changes unless you rely on Express-specific middleware. For high-traffic APIs that is an easy win.

In real applications, framework overhead is rarely what users feel. Database queries, external API calls, missing caching and payload sizes dominate response times. If your API is slow, profile those first. Express 5 is a cleanup and modernization release rather than a performance upgrade, so do not expect big speed changes from upgrading.

Testing

NestJS was designed to be tested. Test.createTestingModule builds a module with real or mocked providers, so you can unit test a service in isolation or spin up the whole app for end-to-end tests with Supertest. New projects come with test scaffolding already in place, with Jest for CommonJS projects and Vitest for new ES module projects in NestJS 12.

Express does not prescribe a testing approach. Supertest works well against an Express app, and Jest or Vitest handle unit tests. The difficulty is not tooling but design: if route handlers import their dependencies directly, mocking them takes extra work. Teams that test Express APIs well usually adopt dependency injection patterns by hand.

GraphQL, microservices and real-time

This is where NestJS pulls clearly ahead for complex systems:

Capability NestJS Express
GraphQL @nestjs/graphql with Apollo or Mercurius, code-first or schema-first Mount Apollo Server or GraphQL Yoga yourself
Microservices Built-in transporters: TCP, Redis, NATS, RabbitMQ, Kafka, gRPC, MQTT Choose and integrate a client library per broker
WebSockets Gateways with Socket.io or ws, using the same guards and pipes Add Socket.io or ws alongside your routes
Scheduled jobs and queues @nestjs/schedule and BullMQ integration node-cron, BullMQ wired manually
OpenAPI docs @nestjs/swagger generates docs from decorators swagger-jsdoc or hand-written specs
Validation Pipes with class-validator or Standard Schema Zod, Joi or express-validator per route

Express can do every one of these things. NestJS simply gives you a consistent, documented way to do them, so the fifth integration looks like the first.

Team size and project type

  • Solo developer or small team, short-lived project: Express. A webhook handler, a backend-for-frontend or a small internal API does not need modules and dependency injection.
  • Small team, product with a long future: either works. Pick NestJS if you expect the team or the API to grow, Express if your developers already have a structure they trust.
  • Three or more backend developers, or several teams: NestJS. Shared conventions reduce code review friction and make onboarding faster.
  • Enterprise integrations, microservices or GraphQL: NestJS, for its built-in transporters and modules.
  • Maximum throughput with minimal overhead: Fastify on its own, or NestJS with the Fastify adapter.

If you are also deciding between Node.js and PHP for the whole backend, our MERN stack vs Laravel comparison covers that decision.

Should you migrate an existing Express API?

Not just because NestJS is popular. A working, well-tested Express API is a fine asset. Migration makes sense when the codebase has become hard to change, new developers struggle to find their way or you are about to add GraphQL, queues or microservices. Because NestJS runs on Express, you can migrate one feature module at a time, reusing existing Express middleware along the way.

Need help choosing?

We build and maintain APIs on both frameworks, and we will recommend the one that fits your team and roadmap. We take on standalone API builds and full web app MVPs, which typically take 6 to 10 weeks, and quote each one individually after a free call. Our NestJS and Node.js developers work with code reviews, QA and full source-code handover on every project. See how our pricing works, or describe your API and we will send a fixed, itemized quote.

Frequently asked questions

Is NestJS built on Express?

By default, yes. NestJS uses Express as its HTTP layer, and since NestJS 11 that means Express 5. You can switch to Fastify with the official adapter, and NestJS's own abstractions keep most of your application code independent of the adapter you choose.

Is NestJS slower than Express?

Slightly, in synthetic benchmarks, because NestJS adds routing and dependency injection on top of Express. In real APIs the difference is usually negligible next to database and network time. If throughput matters, running NestJS on the Fastify adapter typically outperforms plain Express.

Should I migrate my Express API to NestJS?

Only if structure, testability or onboarding has become a real problem, or you are adding GraphQL, queues or microservices. Since NestJS can use Express middleware, you can migrate gradually, one feature module at a time. A well-organized Express API that your team is happy with does not need rewriting.

Do I have to use TypeScript with NestJS?

Technically, you can write NestJS in plain JavaScript, but almost nobody does, and the documentation and tooling assume TypeScript. Its decorators and dependency injection work best with TypeScript's type information. If your team does not want TypeScript, Express is the more natural fit.