As expected when posting, some users claim that I picked one specific sample. I can't put all samples here that I found, this blog post would be endless. You are very welcome to check the code yourself, with Code Complexity linters and your own thoughts enabled - it's OSS.
Clickbaity title? Maybe. But it gets the gist. And before you think "this is just another hate post / opinion", well read on, I digged the source code deeper than many ever go and I did so for good reason.
Also, I've used tools to prime and underline this with facts. See all of my projects aren't perfect - at all - nor is my code. But, let me tell you that I've been migrating the hugest enterprise stacks with integrated linting in the pipelines from repo to jira, so I know what it means to fix "bad" code - whichever way you define it.
I care, a lot!
Next.js is my go-to framework for projects. This is why I care so much about it and why I do this blogpost, because the insights honestly scared me.
Funnily there are often many individuals that think I'm "hating" on Next.js. There is in fact no tool I use more than Next.js. I like using it. Even more so it's important to adress it's problems. Also I did write Lee Robinson a message before posting this but he didn't respond. So did I tag the CEO in a Twitter thread
Why did I even check the Next.js source code?
Well, funnily, I didn't with the intention to write a blog post. In fact, I wanted to find out why setting a value on the request
object inside of the middleware won't be available in it's subsequent renderings (like e.g. the page component). This isn't obvious at all. As it's the same request, you can easily expect it to be reused - it's part of the same code logic / loop as I found later so even more so that would be a reason. But it's not. And I wanted to know why.
I often dig into foreign source codes of big projects to understand what happens under the hood. Mostly to provide the correct solution for my projects. Usually, I get a good feeling after like 1 hour. E.g. digging into any repos of the Supabase organization on GitHub, although it's complexity, is always kinda straightforward.
Now with Next.js, it was very very very different. I started digging on a Sunday, with a few breaks in between, I found the reason for the behaviour on Tuesday. It's not like I didn't have an assumption, but I had to prove that my assumption is correct. I expected a few files to be involved but not this mess only to find out one single thing.
I created a Notion page to keep track of what I'm seeing because the complexity was so high that it was impossible to keep it in my head. The resulting page that didn't even include everything that really happens (I left out a few things that I felt weren't needed to understand the whole thing) had 3300 characters, 430 words. That's a short blog article basically (i would give you a screenshot, but the notes are german and I didn't want to translate all of it).
Edit: Used ChatGPT, here's a screenshot. Please note that this might look "normal" to you by just reading it, but this isn't even complete and pathtracing this took ages, there are extreme amounts of conditions, function calls, etc.
Okay, so will you tell us the result at least?
Sure! NextJS resolves routes all together (interesting, right?) and then loops them (tl;dr version).
The request object is then copied. But not because it makes sense (it does make sense but given the code naming, it's not because of that) but because it needs to be converted and extended with NextJS values. The specific code is const normalizedReq = this.normalizeReq(req)
and it's being passed the original, non NextJS-extended req
object and if normalizeReq
recognizes that it's not a NextRequest
it will make one by copying the values of the given req
. Weirdly, if one would start contributing that isn't aware of that exact thing, the behaviour could change to sometimes actually being the same object (if it's of the NextRequest
instance already). But yeah, let's not discuss this code detail here.
Again: although it makes sense, it is all but obvious.
Let's define bad code
In very short terms: Bad code isn't just technically bad code (like code with failures). Bad code can be code that works perfectly but is so convoluted that it's only understandable after hours of digging or after asking someone else who is long enough in the code. Bad code can also be code that only holds on to work because there are tests so as long as no test breaks, we can trust it works, but we don't know why. There's multiple definitions of "bad" but none of the definitions is one you'd like your code to be aligned with.
Let's have more source code insights from Next.js complexity / DX
I've installed two rather well-known utilities, the SonarLint (very well-known) and the CodeMetrics (https://github.com/kisstkondoros/codemetrics).
In fact I didn't need those tools to determine that NextJS is a big bunch of "wtf is going on here?". Try it on your own, check out the Next.js repo and make it your task to find e.g. "where does Next.js call server actions". Tell me how much time it took (FYI, I don't know this, i just picked a random question that came to my mind).
Let's take another sample of the same file (which is small with ~800 lines considering the fact that I've seen some with 3k+ lines):
resolve-routes.ts#232
:
Before we get to the checkTrue()
, I give you this: As the filename states, resolve routes resolves routes (didn't think of that, right?). What do you think checkTrue()
does - besides the fact that it's body has increased complexity? Does it check true? True for what? Can't you tell? Me neither. There's also no comment that explains it. I cannot think of a single use-case, no matter the project, where it would make sense to define a function named checkTrue()
. I cannot.
I'll give you a bit more code, just the start:
How about now? checkLocaleApi
? Didn't we developers agree that a well-readable code with if's should actually state what it does? Like isLocaleaApi
? And what does that one do? Can we hover it?
Ah, perfect, it returns true or undefined. That clears it up. Not. Given it'd be named isLocaleApi
I could at least make some sense of it. Although, a quick function definition comment wouldn't harm, with maybe even references to related files, would it?
I'll stop with this now. Every single file within the Next.js repo that has a few hundred lines has stuff like this all over the place. It's not "one pick". It's everywhere. Go have a look for yourself.
That's because Next.js is a complex project!!!!1111
There's one thing I can certainly say: Overall Complexity of a project (it being big having many features and dependencies) does never justify bad DX. Never. It explains it, but it doesn't justify it. Never in my career have I noted: Let's make this shit complex as fuck because it's requirements are complex. In fact, quite the opposite. The more complex, the more I try to seperate and structure.
I have contributed in big projects where I didn't know the language even, simply because the code was well-written and well-structured so I could follow along and contribute.
Given semantical complexity in fact is a major, major reason to remove the complexity in the code. This can always be done. By using code quality linters, by defining max file sizes, by having proper, clean code reviews, etc., you name it.
There is one rule that goes way beyond development and more often than never has proven to be effective: Conquer and divide. For instance, team splitting with responsibilites on certain features with splitted repos (can be a monorepo with separated folders, whatever) will lead to teams owning these features and overall often more code but more code that is well structured - if done right.
But David, do you always create perfect projects?
In fact, sometimes I do know that my code is shit code (is it a skill to accept ones own shit code?). But in own projects, you can do so. I didn't need superclean code in my landingpage. Who cares? Even more so, sometimes doing an MVP with 100% shitcode and then throwing it away and redoing it is also a thing I've done and it was worth it.
I do however care in a fintech banking application or generally in contexts that need longevity and fast onboarding. So, DX depends very much on the context but I'd say having an open-source framework with millions of end users, that's a good point to think about awesome code clarity, not even "okayish" but awesome.
What's your take away now David?
I don't know really. I'm not hating. I'm desperate due to what I saw. I tried to reach out to some Vercel guys but I'm not sure if they didn't read or just don't wanna hear it. I'd love to hear some thoughts from them, hearing their opinion on the status quo, to see their actually listening.
Like I'd be interested if there is a method within their team that tries to "decrase code complexity" which goes hand in hand with e.g. linters because I'd really be interested if they just said "Okay SonarLint, complexity of 300 is just GREEN all the way".
What's for sure: I won't, no actually I cannot contribute to Next.js in it's current state as it's, without a doubt, the only project I've seen which was so convoluted in so many different places that I consider my old, hated enterprise project with full-blown shitcode easier because it was well-structured and I could migrate it step by step (took about 2 years, but we did it).