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

Contents
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?
Is Next.js faster than Express.js?
How do I run Express.js alongside Next.js using a custom server?
Should I use NextAuth.js or Passport.js with Express.js for authentication?
How does Express.js compare to Next.js for building a REST API?
How does NestJS fit into the Express.js vs Next.js comparison?
When should I use Express.js instead of Next.js?
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.
