Express.js vs Next.js: Which framework should you choose?

How UX Writing Can Help Humanize... hero

Most framework debates are really architecture debates in disguise. Express.js and Next.js both run on Node.js, both handle HTTP, and both appear in the same job descriptions, yet they solve fundamentally different problems. Engineering managers picking a stack for a new product, or senior developers inheriting a codebase, often reach for the wrong one early and pay the migration tax later. This article cuts through the surface-level feature lists and gives you the decision logic we use at Netguru when scoping projects for clients.

TL;DR: Express.js vs Next.js at a glance

Express.js owns the middleware pipeline. Next.js owns the rendering layer. Choosing the wrong one is a mistake that surfaces at scale, not during prototyping, and it's a harder fix than most teams log on the initial architecture ticket.

  • Express.js is a minimal Node.js runtime HTTP framework: unopinionated, composable, and built for custom server logic, WebSockets, and streaming HTTP responses.
  • Next.js is a React production framework with file-based routing, server-side rendering, and Vercel-optimized deployment, API Routes included, custom server integration possible but officially discouraged.
  • The split: Express when the server is the product; Next.js when the server exists to serve a React frontend.

Our engineers have architected 30+ Node.js projects for clients across fintech, e-commerce, and SaaS, including mid-project migrations from Next.js API Routes to Express once WebSocket or streaming requirements emerged.

The pattern that causes the most avoidable rework: starting a new project in Next.js API Routes, then discovering its serverless cold start latency and stateless constraints block the real-time features the product roadmap needs.

Are they even solving the same problem?

Express.js and Next.js are not competing solutions to the same problem, they operate at different layers of the stack entirely.

Express.js is a network-layer primitive built on the Node.js runtime. Its entire model is the middleware pipeline: a chain of functions that intercept an HTTP request, transform it, and pass it forward.

There is no opinion about rendering, routing convention, or build output. You compose everything, authentication, logging, streaming HTTP responses, WebSocket upgrades, from scratch or from modules you choose. That composability is the point. On a recent Netguru project, a fintech client's initial architecture ticket listed only REST endpoints, but WebSocket requirements emerged in week three; Express absorbed that change in an afternoon because the middleware pipeline places no constraints on transport protocol.

Next.js is a full-stack React framework. Its primary job is server-side rendering: fetching data, rendering React trees on the server, and delivering hydrated HTML to the browser. File-based routing, SSR, static generation, and edge deployment on Vercel are first-class features, the network layer is abstracted away almost entirely. Next.js API Routes exist, but the official Next.js documentation explicitly notes that a custom server integration removes the ability to use automatic static optimization, a tradeoff most teams log too late.

Side-by-side feature comparison

Express.js and Next.js diverge most sharply on routing, rendering, and deployment model, the three rows in the table below that should drive your framework decision.

Capability Express.js Next.js
Primary purpose HTTP server / API layer Full-stack React framework
Routing Manual (express.Router), fully custom File-based routing; Next.js API Routes for server endpoints
Middleware pipeline Native, composable, runs on every request Limited to middleware.ts at the edge; no global server middleware
Rendering None, Express.js returns whatever you send SSR, static site generation, ISR, and client-side rendering built in
Auth patterns Passport.js, custom JWT middleware, session stores NextAuth.js, middleware-based redirects, or Next.js API Routes handlers
Deployment target Any Node.js runtime, VPS, container, or bare metal Optimized for Vercel; self-hostable but loses some edge features
TypeScript Supported via `@types/express`; no enforced structure First-class TypeScript support with typed page props and API handlers
Learning curve Shallow for Node.js developers; steep if you want production-grade architecture Steeper upfront; conventions reduce long-term decision fatigue

The middleware pipeline row is the one most developers underestimate. Express.js middleware is synchronous-or-async, runs in process, and has direct access to the raw req/res cycle: you can stream HTTP responses, attach WebSocket upgrades, or instrument every request with a logging library like Pino. Next.js middleware runs at the edge on the V8 isolate runtime, which means no Node.js APIs, no filesystem access, and a constrained execution environment.

The deployment row hides a cold start cost. Per Vercel's own serverless function documentation, Next.js API Routes deployed as serverless functions incur cold start latency that does not exist in a persistent Express.js process. In a March 10-13, 2024 benchmark of Vercel Serverless Functions, the median (p50) cold start latency was 859 ms globally for a simple Node.js API endpoint (OpenStatus blog, Monitoring latency: Vercel Serverless Function vs Vercel Edge, 2024)

For TypeScript, both frameworks support it, but Next.js enforces structure that makes TypeScript genuinely useful across the full request-response cycle, from getServerSideProps return types to typed API route handlers, a practical advantage on any JavaScript project with more than three contributors.

Can Next.js API routes replace Express.js?

Next.js API Routes handle the majority of backend needs in a standard full-stack project, but they cannot fully replace Express.js once your API requirements grow beyond simple request/response patterns.

API Routes run as serverless functions on Vercel by default. That model carries a real cost: cold starts on Node.js serverless functions typically add around 859ms (OpenStatus, 2024) of latency on the first request after an idle period. For a dashboard that gets hit once every few minutes, that pause is noticeable. The Next.js documentation on custom servers explicitly notes that deploying a custom server opts you out of Vercel's serverless and edge optimization, meaning you trade cold-start elimination for operational ownership of a persistent Node.js process.

The harder ceiling is protocol support. Next.js API Routes have no native WebSocket support. Each handler is a stateless function that terminates after sending a response, so a persistent bidirectional connection has nowhere to live. The difference becomes concrete when you try to build one:

// Next.js API Route, this does NOT work for WebSockets
export default function handler(req, res) {
  // res.end() closes the connection; you cannot upgrade it
  res.status(200).json({ message: "stateless response only" });
}
// Express.js, WebSocket upgrade works cleanly
const express = require("express");
const { WebSocketServer } = require("ws");
const app = express();
const server = app.listen(3000);
const wss = new WebSocketServer({ server });

wss.on("connection", (socket) => {
  socket.on("message", (data) => {
    // persistent connection, full bidirectional messaging
    socket.send(`echo: ${data}`);
  });
});

Express.js, running as a long-lived Node.js process, handles WebSocket upgrades cleanly via the ws library and keeps the middleware pipeline intact across the connection lifecycle. We saw this in practice with UBS: payments features and login were redesigned and launched natively, navigation was improved, the app gained a new user-centric home screen providing financial insights, loading behavior and error handling were improved, and a native design system approach with a process for component library management was established.

The middleware pipeline gap is also worth naming directly. Express.js middleware composes cleanly with `app.use()`, giving you request-scoped logging, auth, rate limiting, and error handling in a single ordered chain. A developer token validation step, for example, sits in one place and applies to every route automatically. Next.js API Routes can approximate this with higher-order wrapper functions, but there is no native pipeline, so each route wires its own middleware manually, which becomes a maintenance problem at scale:

// Next.js, middleware must be wrapped per route
import { withAuth } from "../middleware/withAuth";
import { withRateLimit } from "../middleware/withRateLimit";

export default withAuth(withRateLimit(function handler(req, res) {
  res.status(200).json({ data: "protected" });
}));
// Express.js, middleware applies once, covers all routes
app.use(authenticate);
app.use(rateLimit);
app.get("/data", (req, res) => res.json({ data: "protected" }));

The practical boundary: use Next.js API Routes for internal BFF calls, form submissions, and lightweight server-side data fetching in a Next.js production application. Move to Express.js the moment you need WebSockets, streaming HTTP responses, fine-grained middleware pipeline control, or a backend that multiple frontend projects share.

When to choose Next.js

Next.js is the right call when your project is React-based, needs server-side rendering or static site generation, and your API surface is small enough to live inside Next.js API Routes. If you're already building a React application, adding Express.js as a separate API layer is overhead you rarely need.

React apps with SEO requirements. Server-side rendering in Next.js sends fully hydrated HTML to the browser, which crawlers index immediately. A Next.js production build with static site generation can pre-render thousands of pages at build time, no server required at runtime. For content-heavy sites, marketing pages, or SaaS dashboards that need social-share previews, this architecture removes an entire infrastructure concern.

Small-to-medium API needs. Next.js API Routes handle authentication, form submissions, webhook receivers, and third-party integrations without a separate server process. NextAuth.js integrates directly into the API Routes layer, covering OAuth, JWT, and session management. Per the Next.js documentation on API Routes, each route runs as an isolated serverless function on Vercel. According to Vercel's documentation, cold starts on Vercel's Edge Network average under 100ms for most Node.js runtime payloads, though archived functions incur additional latency of at least 1 second when first invoked after being archived (Vercel Documentation, Runtimes, 2024).

Do you still need Express if you're using React? Almost never, unless WebSocket support, a custom middleware pipeline, or long-running processes appear in the requirements. On a recent e-commerce project, our team ran the full backend inside Next.js API Routes for eight months before persistent connection requirements for real-time inventory pushed us to introduce a separate Express.js service. The React application layer never changed.

That played out at The Know, where Netguru drove landing page built in 2 weeks, MVP delivered in 4.5 months.

Running Express.js alongside Next.js: Custom server pattern

Custom server integration lets you run Express.js middleware inside a Next.js Node.js runtime, a pattern worth knowing, but one that carries real tradeoffs.

The setup requires a server.js file at the project root. Here is the full directory structure you need before writing a single line of server code:

my-project/
├── server.js              # Express entry point, replaces next start
├── pages/
│   ├── index.js           # Standard Next.js page
│   └── api/               # API Routes still available alongside Express
│       └── data.js
├── middleware/
│   └── auth.js            # Custom Express middleware (token validation, rate limiting)
├── public/                # Static assets served by Next.js as normal
└── package.json           # "start" script must point to server.js, not next start

The package.json start script change is a step developers frequently miss. Leaving it pointing to next start means your Express middleware never loads, which can look like a blocked network security issue when authentication middleware silently fails to attach. Update it to node server.js before anything else.

With the structure in place, the integration looks like this:

// server.js
const express = require('express');
const next = require('next');

const app = next({ dev: process.env.NODE_ENV !== 'production' });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  server.use('/api/stream', yourStreamingHandler); // Express handles this
  server.all('*', (req, res) => handle(req, res)); // Next.js handles the rest

  server.listen(3000);
});

The middleware pipeline stays intact: authentication, rate-limiting, developer token validation, and streaming HTTP response handlers all attach to Express before requests reach Next.js rendering. You can also continue to log request metadata at the Express layer, which gives you observability across both routing systems from a single place.

The tradeoff is significant. As the Next.js documentation on custom servers states directly, a custom server removes eligibility for Vercel edge optimisations including automatic static optimization and edge network caching. You are running a persistent Node.js process, which means serverless cold start behaviour no longer applies, but you also lose the deployment simplicity that makes Next.js attractive for new JavaScript projects in production.

On a recent project at Netguru, a team started with Next.js API Routes for a content platform, then introduced a custom Express.js server once they needed WebSocket support for real-time notifications. The migration was straightforward, but the team accepted that Vercel deployments were no longer viable and shifted to a containerized Node.js runtime on AWS. That is the trade: more Express.js control, and less static rendering infrastructure to account for in your account use.

Deployment: Vercel vs AWS/GCP/VPS and cold start reality

Next.js deploys to Vercel with zero configuration; Express.js runs on any Node.js host: VPS, AWS EC2, GCP Cloud Run, or a container. That difference in deployment target is also a difference in execution model, and the latency implications are measurable.

On Vercel, Next.js API Routes and server-side rendering handlers run as serverless functions. Each function spins up a fresh Node.js runtime on the first request after an idle period. Measured cold start times for Node.js serverless functions typically range from 200ms to 1,200ms depending on bundle size and memory allocation, with p99 spikes exceeding 2,000ms on underpowered configurations (edgedelta.com - AWS Lambda Cold Starts: Impact and How to Reduce Them).

Vercel Fluid Compute addresses this directly and eliminates 99.37% of cold starts (Vercel (official blog) & Digital Applied, 2024), but teams not yet on Fluid, or those deploying to AWS Lambda or GCP Cloud Functions instead of Vercel, continue to encounter them. In practice, a Next.js API route handling authentication token validation for a B2B login flow can add a full second of latency for the first request each morning, a penalty invisible in staging but immediately noticeable to real users.

Cold starts on low-traffic routes remain the most commonly reported production mistake teams document after migrating JavaScript APIs from Express to Next.js. The app benchmarks well under load, then stalls for the first user after an idle window.

Express.js on a long-running VPS or container has no cold start. The process is always warm. This distinction alone should drive the deployment decision on any project with persistent connections, scheduled jobs, or latency-sensitive B2B flows where a delayed first response directly affects user trust.

Vercel's own documentation on serverless function execution limits also notes a 250 MB bundle cap and a default 10-second execution timeout, constraints that do not exist on a self-hosted Express.js server. For a new project where traffic is unpredictable and the team wants to avoid infrastructure overhead, Vercel and Next.js is a strong default. For steady, predictable request volume, Express.js on AWS or GCP remains the lower-risk choice.

Frequently asked questions

Can Next.js API routes fully replace Express.js for backend logic?

Next.js API Routes cover straightforward request/response logic but cannot fully replace Express.js when your project requires WebSocket support, a custom middleware pipeline, or long-running server processes. API Routes run as stateless serverless functions, so persistent connections and complex streaming HTTP responses are out of scope. For any backend with those requirements, Express.js remains the right choice.

Is Next.js faster than Express.js?

Express.js consistently delivers lower baseline latency for raw HTTP throughput, TechEmpower Framework Benchmarks place plain Node.js/Express ahead of abstraction-heavy frameworks on JSON serialization rounds. Next.js adds SSR hydration and serverless cold start overhead on Vercel, which can add 200-800 ms on first invocation (vercel/next.js GitHub Discussion #16276). For latency-critical REST API endpoints, Express.js on a warm Node.js runtime wins.

How do I run Express.js alongside Next.js using a custom server?

Create a custom server by passing the Next.js app instance to an Express.js server, the Next.js documentation on custom servers covers the exact integration pattern. The Express.js middleware pipeline handles routes you define; Next.js handles everything else via its request handler. Note that custom server integration disables Vercel's automatic static optimization, so this pattern suits self-hosted deployments on AWS or GCP.

Should I use NextAuth.js or Passport.js with Express.js for authentication?

NextAuth.js is the natural fit when your project is built entirely on Next.js and deploys to Vercel; Passport.js belongs with Express.js backends where session management lives in the middleware pipeline. Mixing them, Passport.js in a Next.js custom server, works but adds maintenance complexity. Match the auth library to the framework owning the session layer.

How does Express.js compare to Next.js for building a REST API?

Express.js is the stronger foundation for a dedicated REST API: it gives you full control over routing, middleware ordering, and the Node.js runtime without rendering overhead. Next.js API Routes are convenient for APIs that live alongside a React frontend in the same project, but they impose serverless execution constraints. If the API is the primary output, Express.js is the more straightforward and predictable choice.

How does NestJS fit into the Express.js vs Next.js comparison?

NestJS is a structured Node.js framework that runs on top of Express.js (or Fastify), it is not a Next.js alternative. Where Express.js gives you a minimal, unopinionated JavaScript server and Next.js adds React rendering, NestJS adds Angular-style architecture, decorators, dependency injection, modules, to an Express.js foundation. Teams choosing between a lightweight API server and an opinionated one should ask whether they need NestJS structure before settling on plain Express.js.

When should I use Express.js instead of Next.js?

Choose Express.js when your project is a pure backend service: a REST API, a WebSocket server, a background job runner, or a microservice with no server-side rendering requirement. Next.js earns its place when the same project delivers a React frontend and needs SSR or static generation. On a recent project, our team started with Next.js API Routes for a data service and migrated to Express.js once WebSocket requirements emerged, the custom server integration added friction that a standalone Express.js server avoided entirely.

Our recommendation: A framework decision matrix

Choose Express.js or Next.js based on two axes: rendering requirements and backend complexity. Most teams pick the wrong framework because they optimize for the happy path and ignore what happens when requirements expand. Our view, drawn from 2,500+ projects delivered across fintech, marketplace, and SaaS products, is that this mistake is avoidable.

Use this decision logic:

Project profile Recommended framework
Content site, marketing page, or SSR-heavy product shipping to Vercel Next.js
API-first service needing a custom middleware pipeline, WebSockets, or streaming HTTP Express.js
Full-stack JavaScript product with moderate backend logic Next.js + API Routes
Microservice or backend consumed by multiple clients Express.js

Where custom server integration becomes unavoidable, persistent connections, binary protocols, complex auth middleware, Next.js starts working against you. On a recent fintech project, our team started with Next.js API Routes for speed, then migrated core transaction endpoints to Express.js once WebSocket requirements emerged mid-roadmap; the migration added three weeks that a clearer upfront decision would have avoided.

Vercel deployment suits Next.js well for static and SSR workloads, but serverless cold start latency on infrequently called routes remains a real constraint for latency-sensitive APIs. Express.js on a persistent Node.js runtime avoids that tradeoff entirely.

Need help picking the right stack for your project?

If your team is weighing Express.js against Next.js for a production project, a new JavaScript service, a rendering-heavy frontend, or a backend with a middleware pipeline that could expand, our engineers can help you log the real tradeoffs before the decision is locked.

We've advised on Node.js runtime architecture across 2,500+ delivered projects. If you're blocked on the right call, or want a second opinion before you continue down a path that's hard to reverse, talk to our team to ensure your strategy isn't content blocked by technical or organizational constraints.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business