Every backend developer has had this afternoon. Something in the framework behaves in a way you did not expect. Maybe a request body gets reshaped before your handler sees it, or an error comes back with the wrong status. You open the debugger, step into the call, and suddenly you are six folders deep in node_modules, reading code that was never written for you to read.
Eventually you find the line. And then you hit the second problem. You can see the fix, but you cannot safely make it. Edit the file in node_modules and the next npm install wipes it. Fork the package and now you maintain a fork. Open an issue upstream and wait.
I have lost more of those afternoons than I want to count. This post is about the other option: own your framework code, keep it in your repo, and still get upstream fixes.
The trade-off nobody says out loud
Dependencies are a great deal right up to the moment you need to understand or change one. Before that moment you get free features, free fixes and somebody else carrying the pager. After that moment you get a black box with a version number on it.
For small utilities this is fine. Nobody needs to read the internals of a date formatter. But a backend framework is different. It decides how every request is parsed, validated, authorized, logged and answered. That is the most important code in your service, and it is usually the code your team has never opened.
The shadcn idea, applied to backends
Frontend people already went through this. shadcn/ui describes itself like this: “This is not a component library. It is how you build your component library.” Instead of installing a package of buttons, you get the actual component code in your project, and, in their words, “the top layer of your component code is open for modification.”
That idea works just as well on the server. Put the framework source in your repo as plain files. Read it in your editor. Set breakpoints in it. Change a line when you need to, in a pull request your team can review, like any other code.
The obvious problem: how do you ever upgrade?
Here is where most people stop, and they are right to. Copied code usually means no more updates. The day you edit the copy, you can no longer just overwrite it with the new version, because you would throw away your change. So the copy slowly rots, security fixes never arrive, and two years later someone writes a migration plan.
If owning the code means losing upstream fixes, it is a bad deal. The whole thing only works if upgrading stays boring.
How it can work: hashes and a three-way merge
The trick is to remember what you started from. At install time, record the version and a hash of every framework file exactly as it was shipped. At upgrade time, you have three versions of each file: yours (what is in the repo now), the original (as shipped) and the new one (from the new release).
- If your file still matches the original hash, you never touched it. Replace it with the new version.
- If you edited it and upstream changed it too, do a three-way merge: base, yours, theirs. Exactly what
git mergedoes. - If both sides changed the same lines, write conflict markers and let a human decide.
Most files will be untouched, so most of the upgrade is a plain replace. The merge only runs where you actually made a decision, which is also exactly where you want a human looking.
Doing it without any framework
You do not need any new tool to start owning code. There are three common manual ways, and each has a cost.
Vendoring. Copy the dependency source into a vendor/ folder and import from there. Simple and fully readable. The cost is the one above: upgrades become a manual diff, and most teams stop doing them.
patch-package. Keep the dependency in node_modules, edit it there, and save the diff. patch-package stores patches in a patches/ folder and reapplies them after every install:
# 1. fix the file in node_modules/some-package
# 2. save the diff to patches/some-package+<version>.patch
npx patch-package some-package
# 3. reapply after every install (package.json)
"scripts": { "postinstall": "patch-package" }This is great for one small fix. Its own README is honest that long-lived patches get expensive when they touch code that changes often, and by default a patch file is all or nothing. You also still debug inside node_modules.
git subtree. Pull the upstream repo into a folder of yours and merge new upstream commits later. You get real merges, but you need the upstream as a git repo, the history gets noisy, and few people on a team remember the commands.
What I built into CalmAPI
When I rewrote CalmAPI for version 2 (I wrote about that in I rewrote my own framework and deleted five of every six files), owning the code became the main idea. npm create calmapi copies the framework into your project as a calm/ folder. The docs put it at around 8,600 lines of plain, commented JavaScript on Express 5 and Mongoose. I counted the framework folder in the 2.0.0-beta.2 package on npm: 8,609 lines across 44 files. A new project has two runtime dependencies, express and mongoose.
Upgrades work the way described above. A calm.lock file records the CalmAPI version and the SHA-256 hash of every framework file as shipped. The upgrading guide shows the flow:
npm install -D calmapi@latest # the new CLI carries the new framework files npx calm upgrade --dry-run # see the plan npx calm upgrade # apply it npx calm doctor && npm test
Untouched files get replaced. Edited files get a three-way merge through git merge-file, so git must be installed. Overlapping edits get conflict markers. If the original files are not available, your file is kept and the new version is written next to it as <file>.calm-new for you to compare by hand. The core of that decision is short enough to read in one sitting:
const pristineHash = lock.core?.[rel];
const untouched = mine !== null && pristineHash
&& sha256(mine) === pristineHash;
if (mine === theirs) {
plan.push({ rel, action: 'same' });
} else if (untouched || !pristineHash) {
plan.push({ rel, action: pristineHash ? 'update' : 'conflict-new' });
} else if (base[rel] !== undefined && sha256(base[rel]) === pristineHash) {
const merged = await merge3(mine, base[rel], theirs);
plan.push({ rel, action: merged.conflicts ? 'conflict' : 'merge' });
} else {
plan.push({ rel, action: 'manual' });
}That is trimmed from the real upgrade.js in the package. And because it ships in the CLI, you can read the whole upgrade logic too. calm upgrade only touches calm/, never your app/ folder. calm doctor lists the framework files you changed locally. And calm about is what its help text calls “the transparency report”: the request pipeline in the order it runs, the headers it sets, what it stores and for how long, what it logs, which hosts the code can reach, and every file the CLI writes, generated from your own calm/ and app/ code.
To be clear about status: 2.0 is in beta. The docs say “APIs may still change before 2.0”, and the current pre-release on npm is 2.0.0-beta.2. It needs Node.js 22.12+ and MongoDB, and you start with npm create calmapi@latest. My tip from the guide: commit before upgrading, and prefer hooks and config over editing calm/. Fewer edits, fewer conflicts.
When owning the code is the wrong choice
I would not push this everywhere. For a weekend project or a throwaway prototype, a normal dependency is less to think about, and you will never upgrade it anyway. And if your team genuinely never reads its dependencies, owning 8,600 lines does not magically make them read it. It just moves the black box into a different folder.
Owning code pays off when the service will live for years, when you have already been burned by a framework doing something you could not see, or when you need to answer “what does this API actually do with a request” without guessing. Vendoring, patch-package, subtree or a framework built for it, use whatever you like. The habit matters more than the tool: know what runs between the network and your handler.
We share more of our open source work on the Broadifi open source page. If you are stuck debugging a framework you cannot see into, write to us at contact@broadifitech.com.

