Express.js built a complete Express 24,430 API: versioned routes, a middleware stack of cors, helmet 10,736 and morgan 8,204 , centralized error handling, role guards, rate limiting. This section has built endpoints in a handful of lines with none of that. The honest question is whether the Express server was wasted effort. It was not — the two solve different problems.
A route handler is right when the endpoint exists for this front end. It deploys with the pages that call it, shares the same session cookie and @/lib modules, needs no CORS, and cannot drift out of sync because there is only one deployment. Webhook receivers, OAuth callbacks, uploads, health checks and an RSS feed belong in app/.
| Question | Route handler | Express service |
|---|---|---|
| Who calls it? | This app's pages | Mobile, partners, services |
| Long-lived connections | No — request scoped | Yes, WebSockets and workers |
| Deploys and scales separately | No | Yes |
| Middleware ecosystem | Write it yourself | cors, helmet, passport |
| Runtime | Whatever Next.js 10,514 runs | Your choice |
The technical line is worth stating plainly. A handler is request-scoped: it starts when a request arrives and ends when the response does, so it cannot hold a WebSocket, run a queue consumer, or keep a connection pool warm across a serverless cold start. Express.js's Socket.IO 24,482 work and any background job need a process that outlives a request. Scaling is coupled too: a burst of API traffic scales your rendering tier with it. Cost runs the other way — Express gives you helmet, express-rate-limit 3,306 and morgan by installing three packages, while in a handler you write each of those.
Most real projects land in the same place: keep the Express API of Express.js as the system of record, call it from Server Components as Consuming Express shows, and use route handlers as a thin edge for work specific to this front end — trimming a payload, hiding a key, receiving a webhook.