Not every web project needs a *web framework*, but when I pick a framework for a project, the main thing I want from it is to _do the things that are hard to get right and that nearly every application needs_. For me, the gold standard is Ruby on Rails. Out of the box, you get * database connections for development, test, and production * database migrations, expressible in either Ruby or SQL, and with `up` and `down` support * sessions that are secure by default, load automatically on first read, and write automatically on change * the Rack API and the full power of community middleware * asset compilation, minification, and caching And that was all in 2012. I can't think of a single JavaScript framework / meta-framework that comes close to the power of Rails. [Astro](https://astro.build/) is my current favorite. I love the focus on content collections and on web-centric APIs like the image optimizer. It still has some catching up to do relative to Rails, Django, and Laravel, especially around interacting with deatabases. I'm also fairly fond of [Remix](https://remix.run/). I really like the focus on web standards like `FormData`. The data-loading and refresh APIs are quite powerful. While I don't have a single favorite, **there is one clear worst choice among JavaScript meta-frameworks: Next.js**. Years ago, I wrote a lot of [Ember](https://emberjs.com/). One of the guiding principles from their core team was "stability without stagnation." New features came regularly in minor versions. Major versions were really about removing support for old ways of doing things, and nearly every deprecation came with a corresponding codemod to help app developers get their apps onto the new patterns. The Next.js core team looked at Ember and said, "Bet. We can do instability _and_ stagnation." ### API Fragmentation In particular, Next.js has at least four completely incompatible APIs for writing an HTTP endpoint. If you have a team writing any moderately large application, you're going to end up with all of the following: `getServerSideProps` in `pages/*` running on the `serverless` (Node) runtime. The function signature is `(GetServerSidePropsContext) => Promise`. The context includes Node's `IncomingMessage` and `ServerResponse`. In typical Node fashion, the `res` exists before the handler is invokes. Redirects and not-found errors have their own special return value structure. `handler` functions in `pages/api/*` running on the `serverless` (Node) runtime. The function signature is `(NextRequest, NextResponse) => void`, where `NextRequest` and `NextResponse` are extensions of the Node `IncomingMessage` and `ServerResponse` APIs. `handler` functions in `app/**/route.{js,ts}` running on the `edge` (web standards) runtime. The function signature is `(NextRequest) => NextResponse`, where `NextRequest` and `NextResponse` are extensions of the web-standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs. `Page` functions in `app/**/page.{jsx,tsx}` running on the `edge` (web standards) runtime. The function signature is `({params, searchParams, request}) => ReactNode`. It's not totally clear what the actual types are because [Next.js's TypeScript docs for the App Router](https://nextjs.org/docs/app/api-reference/config/typescript) don't discuss how to type a Page. But the `request` is definitely a web-standards `Request` or a subclass thereof. ### Centralization is Impossible It's certainly annoying to have to remember all four of those APIs in one app. But that isn't my gripe. My gripe is that it's **impossible** to write a shared library that handles core concerns like authentication, authorization, A-B testing, feature-flagging, or flash messaging in a way that's compatible with all four. You can certainly try. A "simple" request-logging middleware might look like this: ```ts /** * `handler` is a getServerSideProps or a `handler` or a `GET` or a `Page`. * I'm too tired[^1] to try to define the types for all four of those. * * [^1]: I'm tired because I've been fighting with Next.js all day. */ function withRequestLogging(handler) { return function handlerWithRequestLogging(contextOrReq, responseOrUndefined) { if (contextOrReq != null && 'req' in contextOrReq && 'res' in contextOrReq) { // getServerSideProps const { req } = contextOrReq // req.url is called "url" but it's just the pathname console.log(`${req.method} ${new URL(req.url, req.headers.host)}`) return handler(contextOrReq); } if (contextOrReq?.headers != null && 'get' in contextOrReq.headers) { // web standards const req = contextOrReq console.log(`${req.method} ${req.url}`) return handler(contextOrReq, responseOrUndefined) } // Node API route const req = contextOrReq // req.url is called "url" but it's just the pathname console.log(`${req.method} ${new URL(req.url, req.headers.host)}`) return handler(contextOrReq, responseOrUndefined) } } ``` Auth0 makes a valiant attempt at exactly this with their [auth0/nextjs-auth0](https://github.com/auth0/nextjs-auth0) library. The user has to import `auth0` from `@auth0/nextjs-auth0/client` or `@auth0/nextjs-auth0/edge` depending on where they're using it, but the APIs are at least consistent across the two runtimes. But note [this in the readme](https://github.com/auth0/nextjs-auth0?tab=readme-ov-file#using-this-sdk-with-react-server-components): > Server Components in the App Directory (including Pages and Layouts) cannot write to a cookie. That means that if the middleware detects it needs to update the session, **it cannot**. It will just silently fail to update the session. A session middleware can't reliably update the session. A flash-messaging middleware can't reliably mark messages as consumed. A server-timing middleware can't reliably set the `server-timing` response header. This sort of encapsulation of shared logic is _exactly what I want a framework to enable_. I don't want my team to have to maintain a complex list of request guard functions to call in every single endpoint. I want to centralize as much as possible and distribute the detailed configuration. I want to centralize things like `server-timing` and `session`, which don't vary by route. ```ts // middleware.ts export const middleware = stack( serverTimingMiddleware(), cookieSession('my very long secret'), ); ``` I want to distribute configuration for things that do vary by route, and I want to separate it from the routes' core business logic: ```ts export const route = { @requireAuth @requireRoles('User', 'Read') @requireFeatures('2025-01-new-cache') get() { // the actual domain logic } } ``` ### Next.js Middleware I hear some readers responding: "but Next.js has a [first-class Middleware API](https://nextjs.org/docs/app/building-your-application/routing/middleware)!" It's definitely an API. I'd call it steerage at best. The first major problem is that it only runs in the `edge` runtime. The only thing it can pass to the app is strings in the form of HTTP headers. It's not possible, for example, to create a `session` middleware that automatically updates the cookie if a route modifies a value. The second major problem is that there's only one. If I want different behavior at the middleware layer for different routes, I have to do a regular expression match on the route pathname in a giant `switch` statement. ### Summary Every time I join a project using Next.js, I immediately add a ticket to the backlog: "Move off Next.js."