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.

6 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.

ExpressNext.js
What it gives youA request pipelineA render path
RoutingWhatever you writeFile system, with layouts
RenderingNoneThe entire point
Bundling the clientNot its jobBuilt in
Long-lived connectionsNativeAwkward to impossible
What you assemble yourselfMost of a front endMost of a back end

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.

The middleware point deserves more than a bullet, because it is the one that gets discovered late. An Express application is a pipeline you assemble: every request passes through the functions you registered, in the order you registered them, and you can reason about the whole chain by reading one file. Next.js has nothing equivalent at the application level. proxy.ts - the convention that replaced middleware in 16 - runs on a matcher, on requests you may not have intended, and is not a request pipeline. Trying to use it as one is the most common way a team ports an Express habit into a codebase that cannot support it.

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.

The part that is underestimated is not any single item on that list. It is that these decisions interact. Code splitting has to know about routing. Streaming has to know about the data layer. The client boundary has to know about both. A framework is worth what it is worth because somebody made those choices consistent with each other, and a custom stack is where they slowly stop being consistent.

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.

Where route handlers stop being enough

Three limits, in the order teams hit them.

The connection has to stay open. A route handler is built around a request that ends. Server-sent events are possible with a streamed response; a WebSocket is not, and on a serverless deployment target neither survives the function's lifetime. Anything with a persistent connection belongs in a process you control.

Something has to happen on a schedule. There is no job runner. A cron service hitting an authenticated endpoint covers the simple cases, and stops covering them the moment a job needs a retry policy, a dead letter queue, or more time than the platform's execution limit allows.

The API has consumers who are not you. Versioning, deprecation, per-client rate limits, an authentication story separate from your front end's session, and documentation that is a contract. Every one of those is buildable in route handlers and every one of them already exists in the ecosystem you would have chosen instead.

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.

The cost is real and worth stating: two deployment targets, a contract between them that somebody maintains, and types that have to be kept in sync across a network boundary rather than checked by one compiler. Teams that adopt this split without acknowledging that cost end up with a front end that reimplements half the API's rules to avoid a round trip.

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.

Choosing Express because you want control, on a project that is all pages. Control over the render path is what Next.js gives you; control over the request pipeline is what you would be buying, and on a rendering project that is the pipeline you were never going to need.

Related questions

Can Next.js replace our Express API entirely?
For an API consumed only by your own front end, usually yes. For one with external consumers, versioning, rate limits and its own authentication story, you would be rebuilding Express inside route handlers.
Can I run Express and Next.js together?
You can, with a custom server, and it costs you more than it looks - a custom server disables automatic static optimisation for the routes it fronts. Running them as two processes behind a reverse proxy is usually cleaner.
What about WebSockets?
That is one of the clearest lines between the two. A WebSocket connection outlives the request that opened it, and Next.js route handlers are built around a request that ends. On a serverless target they cannot hold one at all. Put long-lived connections in a process you control.
Is Fastify or Hono a better comparison than Express?
For a new API, probably - both are faster and have better typing stories. The argument on this page is not about Express specifically; it is about whether you want a server you compose or a framework that already decided. Substitute your preferred one throughout.

← Back to all articles

Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page