Skip to content

Taking Over a Next.js Codebase Nobody Wants to Touch

The first instinct is to rewrite it. Here is the two-week orientation we run instead, what it produces, and how to tell the difference between a codebase that is bad and one that is merely unfamiliar.

4 min read

A team inherits an application. The people who wrote it are gone. Every deployment is a small event, nobody is sure which parts are load-bearing, and within a month someone says the word "rewrite" in a meeting.

Sometimes a rewrite is right. It is right far less often than it is proposed, and the decision is almost always made before anyone has the information to make it.

Two weeks of reading before one line of writing

The orientation we run on an inherited codebase is fixed: read, map, document, and change nothing. Two weeks, and the deliverable is a document, not a pull request.

What goes into it:

The route map. Every route, its rendering mode, what data it reads, and what it writes. In the App Router this is largely recoverable from the build output and from page.tsx files; in a Pages Router application it means reading getServerSideProps bodies. It is dull work and it is the single most valuable artefact, because "what does this application actually do" turns out to be a question nobody can answer.

The data boundary. Which calls go to which service, which of them are authenticated, and which of them are cached. Watch for the one nobody documented: an internal API called from a client component with a key in NEXT_PUBLIC_.

The deployment path. What builds it, what tests run, what the environment variables are, and what happens when it fails. If the answer is "Ali knows" and Ali has left, that is the highest-priority item in the whole document.

The blast radius of each area. Which files are imported by everything and which are imported by one route. A shared utils.ts with forty exports and a hundred importers tells you where a small change becomes a large one.

What actually makes a codebase hard to change

Having audited a lot of these, the properties that correlate with fear are narrow and consistent.

  • No types at the boundaries. Not "no TypeScript" - most of these projects have TypeScript. It is any at every fetch, so the compiler validates the inside of the application against assumptions nobody checked at the edge.
  • State in more than one place. A server cache, a client store and URL parameters all claiming to hold the same value, with no rule about which wins.
  • Side effects in render. useEffect chains that fetch, then set state, then trigger another effect. These are the changes that break something three screens away.
  • No seam between the framework and the domain. Business rules written inline in route handlers cannot be tested without a request, so they are not tested.

Notice what is not on that list: old dependencies, an unfashionable folder structure, class components, CSS that someone dislikes. Those are visible and cheap to change. They are not what makes deployment frightening, and a rewrite justified on that basis buys a year of risk for an aesthetic improvement.

Making it safe before making it good

The order matters, and it is not the order that feels satisfying.

  1. Get the deployment reproducible. Until you can deploy at will and roll back at will, every improvement is a gamble.
  2. Type the boundaries. Parse external data at the edge - with zod or equivalent - so that wrong data fails at the fetch with a useful message instead of failing three components later as undefined is not a function.
  3. Add tests where the money is. Not coverage. The checkout, the auth flow, the one calculation the business depends on.
  4. Then restructure, one area at a time, behind those tests.

Most of the fear is gone by step two, and step two is usually a week of work on a codebase people were describing as unsalvageable.

When a rewrite is genuinely the answer

There are cases. In our experience, it is worth it when the framework itself is the problem - an application built on an abandoned meta-framework, or one whose rendering model cannot be made to produce the pages you need. It is also worth it when the application is small enough that a rewrite is measured in weeks, in which case the argument barely matters.

It is not worth it because the code is ugly, because it is on an older major version, or because the current team did not write it. Those are the three reasons it is usually proposed.

And if the real problem is that the application is on the Pages Router and the path forward is not obvious, that is not a rewrite either - it is migrating Pages Router to App Router without a freeze, which is a different and far cheaper piece of work.

What you owe the next person

Whatever you decide, leave the document. The reason the codebase was frightening is that the last team's understanding of it left with them, and the only durable fix for that is writing it down where the code can be read beside it.

We hand that map over whether or not the client continues with us. It is the part of the engagement with the longest life.

Back to all articles