Skip to content
5 min read

NestJS or Express for a new Node API

Two defensible choices with different bills. What each costs on day one and in month twelve, and the few conditions that genuinely decide it.

On this page
  1. Express bills you in month twelve
  2. The two inputs that predict it
  3. Things I have watched people decide this on, wrongly
  4. Both frameworks let you make the same mistake
  5. What I ask instead

Both frameworks are production-grade. That is not in question, and any comparison that spends time there is wasting yours. The thing nobody tells you is that you are not choosing between a good and a bad framework. You are choosing when you want to be billed.

Express bills you in month twelve

Day one with Express is a file and a route. It feels like winning.

Month twelve is a codebase where every feature picked its own conventions, because nothing stopped it. Three ways to validate a request body. Two error shapes, depending on who was on call that week. Business logic sitting in route handlers because that is where it fit on the Tuesday it was written.

None of that is an Express flaw. Express is a routing library doing exactly what it says. The conventions were your job, and the framework never once reminded you.

NestJS bills you on day one instead: modules, decorators, a DI container to understand before your first endpoint returns anything. Then month twelve arrives and every feature looks like every other feature, and someone who has read any NestJS codebase can navigate yours.

Here is the part that took me a while to see. The structure NestJS imposes is roughly the structure a disciplined Express codebase arrives at on its own, around month eight, after an argument. The real difference is whether that structure is enforced by a framework or by you, in code review, every week, forever.

The two inputs that predict it

I used to answer this with a feature comparison. Feature comparisons never once changed my mind, and I now think they are theatre.

Two things predict the right answer, and both are about people rather than code.

How many people will touch this service. One developer holds conventions in their head and genuinely does not need help. Four developers do not agree on conventions and never will, and the argument surfaces as inconsistency rather than as an argument. Somewhere around three people on the same service, imposed structure stops being overhead and starts being the cheapest thing you own.

How long it has to live. A service that runs for years accumulates features and loses the people who wrote it. That is the scenario NestJS is priced for. A tool you will delete next quarter does not need to survive its authors, and paying upfront for that is just paying.

Everything else is a tiebreaker. What the team already knows is a real one, and a bigger one than people admit — a team fluent in NestJS ships faster in NestJS, full stop. Where the service sits matters too: a gateway, a webhook receiver, a cron runner, anything that should stay thin, stays thinner in Express. The service that holds your domain is where structure earns back its cost.

So: Express when it should stay small and one or two people own it. NestJS when several people will work on it for years. If those two signals disagree, go with team size.

Things I have watched people decide this on, wrongly

Performance comes up every time and has never been the answer. NestJS runs on Express by default and on Fastify if you ask it to. Either way your response time is database queries and the services you call. Framework overhead shows up in a benchmark and vanishes in production next to one unindexed query.

TypeScript is not a differentiator in 2026. Both are fully typed; NestJS assumes it, Express works with it.

Ecosystem lock-out is not real either. NestJS runs Express middleware.

And "NestJS is enterprise" means nothing. It is a container and a module system. Those exist because large codebases need them, which is a claim about size, not about industry.

Both frameworks let you make the same mistake

This is the bit that outlasts the decision, so if you skim, skim to here.

ts
// Express
app.post("/leads", async (req, res) => {
  const existing = await db.query("SELECT …");          // logic
  if (existing) return res.json(existing);              // logic
  const created = await db.query("INSERT …");           // logic
  await sendNotification(created);                      // logic
  res.status(201).json(created);
});
ts
// NestJS — same mistake, better formatting
@Post()
async create(@Body() dto: CreateLeadDto) {
  const existing = await this.db.query("SELECT …");
  if (existing) return existing;
  const created = await this.db.query("INSERT …");
  await this.notifier.send(created);
  return created;
}

The second one looks like an improvement. It is the same bug wearing a decorator.

Both strand the behaviour inside the HTTP layer. Both are untestable without spinning up HTTP. And both get copy-pasted the month someone needs that same lead capture to run from a queue consumer, which is how you end up with two implementations that drift.

The fix does not care which framework you chose. The handler parses input and delegates. A plain class does the work. Data access sits behind a repository. Do that and the framework choice becomes a matter of taste, and a migration later is a rewrite of the thin layer rather than of the application.

That repository boundary is also where the decisions that bite actually live: whether your writes are idempotent, whether the tenant filter is structural rather than remembered, whether queries belong to a builder or to you. I wrote about the last one in Drizzle or raw SQL. None of it changes with the framework, which is sort of the point.

What I ask instead

Thin services do not stay thin. They sit where the traffic already arrives, and nobody ever schedules the migration, so scope lands on them because landing it there is cheaper than the alternative that week.

So the question is not what this service is today. It is what it will have absorbed in two years, given that it will absorb something. Answer that honestly and the choice mostly makes itself.

Frequently asked

Should I use NestJS or Express?

Express suits small services, a single maintainer and anything you want to keep deliberately thin. NestJS suits several developers, many features and a service expected to live for years, because the structure it imposes is the structure a large Express codebase has to invent anyway. Both are production-grade; the difference is when you pay.

Is NestJS slower than Express?

Marginally, and almost never in a way that matters. NestJS runs on Express by default and can run on Fastify instead. In practice the response time of an API is dominated by database queries and external calls, so framework overhead is rarely the number worth optimising.

Can I migrate from Express to NestJS later?

Yes, and how painful it is depends on one thing: whether your business logic lives in plain functions and classes or inside route handlers. Logic in services migrates by rewriting the HTTP layer. Logic in handlers has to be extracted first, which is the same work you would have done anyway.

Does NestJS lock me in?

The decorators and module system are framework-specific, so controllers and module wiring are a rewrite if you leave. Services, repositories and domain logic are ordinary TypeScript and move freely — which is another reason to keep the framework at the edges and the logic in plain classes.

Found this useful?Share on XShare on LinkedIn

Related reading