close
close

Next.js, why exactly?

I don’t want this to just be a rant. No way. But of all the frameworks I worked with for Lucia, Next.js was consistently annoying to work with. And it hasn’t improved in months.

In Lucia, Auth.handleRequest() is a method that creates a new one AuthRequest for example, which includes the method AuthRequest.validate(). This checks whether the request comes from a trusted origin (CSRF protection), validates the session cookie and sets a new cookie if necessary (this is optional). This requires at least the request URL or host, request method, and request headers. This shouldn’t be a problem as most if not all JS frameworks (Express, SvelteKit, Astro, Nuxt, etc.) provide you with a request object, usually a Request or IncomingMessage.

And then there’s Next.js.

Next.js 12

Next.js 12 and the Pages Router were fine. You get access to IncomingMessage And OutgoingMessage inside getServerSideProps()which allows you to run code on the server before SSRing the page.

export const getServerSideProps = async (req: IncomingMessage, res: OutgoingMessage) => {
	req.headers.cookie; // read header
	res.setHeader("Set-Cookie", cookie.serialize()); // set cookie
	return {};
};

However, there were few problems with it. First, you simply can’t set cookies when you deploy the page in the Edge. You just can’t do that. I’m not sure about the history of Next.js, but it seems like the API hasn’t been properly thought through. Another problem is that the middleware uses the web standard Request. Props to the Next.js team for moving to web standards, but I’d say this just made things worse with inconsistent APIs (IncomingMessage vs Request). But in the end it works…I think.

Next.js 13

It’s a joke that Next.js is production ready.

Next.js 13 introduced a new router: the App Router. All components in it are standard React Server Components, so they always run on the server. Everything is rendered on the server and sent to the client as pure HTML.

// app/page.tsx
const Page = async () => {
	console.log("I always run on the server"); // only gets logged in the server
	return h1>Hello world!h1>;
};

If you’ve ever used Remix, SvelteKit or Astro, it’s similar to the loading pattern. If you’ve used Express or Express-like libraries, this is just one app.get("https://pilcrowonpaper.com/", handler). So you would expect the request or a request context object to be passed to the function… right? Right?

// app/page.tsx
// something along this line
const Page = async (request) => {
	console.log(request);
	return h1>Hello world!h1>;
};

Inconsistent APIs

So, how do you get the request on your pages? Well, the thing is: you can’t! Yes, what a brilliant idea! Let’s go all in with servers and not allow your users to access the request object.

Actually, they do, but they don’t. They do offer cookies() And headers()that you need to import for some reason.

// app/page.tsx
import { cookies, headers } from "next/headers";

const Page = async () => {
	cookies().get("session"); // get cookie
	headers().get("Origin"); //get header
	return h1>Hello world!h1>;
};

Okay fine. Maybe there’s a good reason why they can’t just pass them off as arguments. But why would you only offer an API for accessing cookies and headers? Just export one request() that returns a Request or the request context?. This makes less sense when you realize that API route handlers and middleware give you access to the Request object.

// app/api/index.ts
export const GET = async (request: Request) => {
	// ...
};

And here’s the fun part. You can’t use it cookies() And headers() within middleware (middleware.ts)!

Just give us a single API to communicate with incoming requests.

Arbitrary restrictions

Remember that you could not set cookies getServerSideProps() when the page is running on the Edge? Well, the App Router doesn’t let you set cookies when rendering pages, period. Not even if it runs on Node.js. Wait, why can’t we use it? cookies()?

// app/page.tsx
import { cookies } from "next/headers";

const Page = async () => {
	cookies().set("cookie1", "foo");
	return h1>Hello world!h1>;
};

They set the set() method, but you will get an error message when you try this! Why???? I can’t think of any valid reason why this restriction is necessary. SvelteKit does this well. Any HTTP framework will do this just fine. Even Astro, the framework that focuses on static sites (or at least used to), did this just fine before 1.0.

Also, headers() is always read-only, unlike cookies() which can set cookies within API routes. Another consistency problem.

My final complaint concerns middleware. Why is it always running on the Edge? Why restrict running database queries or using Node.js modules? It just complicates everything and makes passing state between middleware and routes impossible – something Express, SvelteKit, and again, even Astro can do.

Only: why?

All these little issues add up to make supporting Next.js as a library author frustrating at best and nearly impossible at worst. The slow startup and compilation time, as well as the buggy development servers, make using Next.js generally unenjoyable. Caching is a whole other issue that I haven’t touched on either.

I don’t want to assume anything malicious on the part of Next.js or Vercel, but they just seem to be outright ignoring cookie setting issues inside page.tsx. Their developer is pretty good at responding on GitHub and Twitter, but they haven’t responded to tweets or Github issues about this. Their developer and even the CEO contacted me to ask if there was anything that could be improved, and I mentioned the cookie issue, but no response. I even tweeted at them several times. I don’t expect any changes, especially not immediately, but some kind of acknowledgment would be nice.

I get it. I shouldn’t expect anything from open source projects. I am a library author myself. But come on. It’s a huge framework, backed by a huge company. Is it bad to have certain expectations?

I think the cause is twofold. Firstly, a rushed release. The documentation is still poor and everything seems to be more or less incomplete. And second, specifically React and server components. React is still trying to be a library, while at this point it’s definitely a framework. The hassle of Next.js APIs and React APIs with overlapping responsibilities on the server doesn’t work. React needs to embrace a single framework, whether it’s their own or Next.js, and fully commit to it.

Update: I’m told some of these issues stem from streaming. You cannot set status codes and headers after streaming has started (everything is streamed in RSCs). I don’t see how that makes anything better. It just makes them look worse; They were aware of the problem, yet built a whole framework around it without addressing it.