Skip to content

Next.js vs Express: Only One of Them Renders

Express is a server you build on. Next.js is a rendering framework with a server attached. Choosing between them is really choosing how much you want to write yourself.

3 min read

These two are compared because both are Node and both serve HTTP, which is roughly where the similarity ends. Express is a minimal HTTP framework you compose. Next.js is a rendering framework that happens to include a server.

The decision is not really a comparison. It is a question about what you are building.

If you are building an API and nothing else

Use Express, or Fastify, or Hono. Next.js route handlers exist and they work, but you would be taking on a rendering framework, a build step, a file-system router and a React dependency for a service that renders nothing.

Specifically, Express stays better when your API has:

  • External consumers. Versioned routes, API keys, documented contracts.
  • Its own middleware chain. Rate limiting, request validation, custom authentication, logging that belongs to you.
  • Anything long-lived. WebSockets, server-sent events, connections that outlive a request.
  • Deployment constraints. A plain Node process is a simpler thing to run on infrastructure that has opinions.

If you are rendering pages, do not build it yourself

The reverse is stronger and less obvious.

Teams that start with Express and add React on top end up writing, by hand, the things Next.js already decided: routing, code splitting, data fetching on the server, streaming, the client bundle boundary, image handling, caching. Each is a week. Together they are a framework, and it will be a worse one, because it is a side project rather than the product.

If your application has pages, the case for Express is the case for writing your own framework. That is occasionally correct and usually not.

Where route handlers are genuinely enough

For an API consumed only by your own front end, Next.js route handlers do the job:

// app/api/products/route.ts
export async function GET(request: Request) {
  const products = await db.product.findMany();
  return Response.json(products);
}

One deployment, one codebase, shared types between the API and the components that call it. For most product applications this is the right amount of machinery.

Worth knowing: for data your own pages need, you frequently do not need a route at all. A Server Component can query directly, and a Server Action can mutate - the round trip through your own HTTP API is a habit carried over from a client-rendered architecture.

The arrangement most teams actually end up with

Express - or Fastify, or a Go service, or Laravel - holding the business logic and the jobs. Next.js holding the front end. They talk over HTTP.

This is the same conclusion as Next.js against Laravel, for the same reason: the render path and the business logic are different problems, and frameworks are good at one of them each.

What not to do

A custom Express server in front of Next.js. It is supported and it disables automatic static optimisation for the routes it fronts - which is most of what you came for. If you need a proxy, put one in front of both processes instead of inside one of them.

Porting an Express middleware chain into proxy.ts. The convention that replaced middleware in 16 runs on Node and is not configurable, and it runs on requests you did not intend. It is for routing decisions, not for a request pipeline.

Back to all articles