Skip to content
SJ
All writing
9 min read

Express, Fastify, or NestJS

You are not choosing throughput. You are choosing how much structure the framework will force on you — a team-size question wearing technical clothes.

Node.jsNestJSExpressFastifyArchitecture

Every comparison of Node frameworks opens with requests per second, and it is close to irrelevant. Your handler is going to spend six milliseconds waiting on Postgres. The framework's share of that is noise, and no product has ever failed because its router was twenty percent slower.

What you are choosing is how much structure the framework imposes before it lets you write a feature — and whether that structure is a gift or a tax depends on facts about your team, not about the framework.

The real axis

Every codebase converges on some set of answers: where validation happens, how errors become responses, how a request-scoped logger reaches a service four calls deep, how dependencies are constructed and replaced in tests, how modules are kept from importing each other arbitrarily.

You will answer those questions whichever framework you pick. The choice is only whether the framework answers them for you, badly for your particular case but consistently, or leaves them to you, perfectly for your case but differently in each folder once three people have contributed.

Express

A router, middleware, and nothing else. That is genuinely a feature: you can read the entire mental model in an afternoon, every question has a decade of answers, and there is no framework behaviour to fight.

What it costs shows up in year two:

  • No convention means five conventions. Three routers validate with three libraries. Error handling is a mix of next(err), thrown exceptions and manually written status codes.
  • Async errors are not automatic. An async handler that rejects without a wrapper does not reach your error middleware. Express 5 improves this, but a large amount of real-world Express is still version 4 with a hand-rolled asyncHandler.
  • Wiring is manual. Constructing services and passing them down is fine at ten modules and tiresome at a hundred, and it drifts toward module-level singletons, which are the thing that makes tests need real infrastructure.

Express is the right answer for a small service, a prototype, a team of one or two, or anything with a short expected life. It stops being the right answer at roughly the point where people who did not write the original code start adding to it.

Fastify

Fastify is often introduced as “faster Express,” which undersells the two things that actually matter about it.

Schema-first routes. You declare a JSON Schema for the body, params, query and response. Validation happens before your handler runs, the response serialiser is compiled from the schema, and your API documentation is generated from the same declaration. One artifact does three jobs, and they cannot drift apart.

Plugin encapsulation. Plugins form a tree, and what a plugin registers is visible to its children and not to its siblings. That is a real module boundary — a database connection or an auth decorator scoped to one branch of your app rather than bolted onto a global object. It is closer to a dependency system than middleware usually gets.

The response serialisation deserves a note, since it is the one place the speed claim is genuinely meaningful: serialising only the fields in your schema is both faster and safer, because a field you forgot to remove from an entity cannot leak into a response that never declared it.

The cost is a smaller ecosystem and a plugin model that has its own rules to learn. Fastify suits teams who want structure at the edges — contracts and boundaries — without adopting an application architecture.

NestJS

Nest brings an opinion about the whole application: modules, dependency injection, and a request pipeline split into named layers — guards for authorisation, pipes for validation and transformation, interceptors for cross-cutting concerns, filters for turning exceptions into responses.

What that actually buys, in order of value:

  • Dependency injection makes tests cheap. A service declares what it needs in its constructor and the container provides it, so a test provides a fake instead. No module mocking, no import-order tricks, no real database to unit-test a pricing rule.
  • Cross-cutting concerns have one obvious home. Logging, auth, timeouts, response shaping and error translation each live in a layer rather than being repeated per handler — so they are applied consistently by construction rather than by discipline.
  • Structure is discoverable. A new developer finds the module, the controller, the service. That sounds trivial until you have onboarded someone onto an Express codebase where every folder is a different person's idea of good design.

The costs are real. There is meaningful framework knowledge to acquire before you are productive — decorators, providers, module scoping, and circular dependency errors that are genuinely confusing the first time. A simple endpoint takes more files. And for a service with four routes, all of that is pure overhead.

Worth knowing: Nest is an architecture layer, not an HTTP server. It runs on Express by default and on Fastify with a one-line change, so the structure decision and the transport decision are separable.

Choosing

Reduced to the questions that actually predict regret:

  • How many people will touch this, over how long? One person for six months: Express. Five people for five years: Nest. The value of imposed convention scales with the number of people who would otherwise invent their own.
  • Is the API a contract with someone else? If other teams or external customers consume it, Fastify's schema-first approach — or Nest with validation and generated OpenAPI — pays for itself in the arguments you do not have.
  • How much business logic is there? A service that maps HTTP to database calls needs no architecture. One with pricing rules, state machines and integrations benefits from being testable without HTTP, which is what DI actually delivers.
  • Is it going to be split later? Nest's module boundaries make extracting a service a mechanical operation rather than an archaeology project.

The honest default for most product teams is Nest for anything expected to live years, Fastify for a focused service with a real contract, and Express for the genuinely small.

Migration cost, which nobody prices

Migrations between these are asymmetric, and it is worth knowing the direction before you commit.

Express to Nest is a rewrite of composition, not of logic. Handlers become controller methods, business code moves into services largely unchanged, and it can be done route by route with both running side by side. Tedious, not risky.

Nest to Express is harder than it looks, because DI is not a layer you peel off — you have to construct by hand everything the container was constructing, and the request pipeline layers have to be reimplemented as middleware with the ordering preserved.

Which suggests the tiebreaker when you genuinely cannot decide: starting structured and simplifying is easier than starting loose and imposing structure onto code that grew without it.

The short version

Ignore the benchmarks; the database is your latency. Express when the codebase is small and short-lived, Fastify when the contract at the edge is the important part, Nest when several people will maintain substantial business logic for years. And weigh migration direction — adding structure later is the expensive one.

Written by Saumya Jain

Full Stack Engineer working on headless commerce, NestJS microservices, and real-time systems. Currently open to remote work.