I Tried Sharing Types via a Private NPM Package. It Was a Disaster.

Sethonne June 28, 2025

For solo developers or small teams, this method is a trap disguised as good architecture. Don’t fall for it. Use tRPC. Use turborepo. Hell, copy and paste the types if you have to. Just don’t build a pipeline that adds more friction than value.

I Tried Sharing Types via a Private NPM Package. It Was a Disaster.

In one of my recent projects, I attempted to share TypeScript types between my frontend and backend using the reddit-approved method of bundling them into a private npm package hosted on GitHub Packages.

I regret everything.

"Don't Repeat Yourself", they said, as they glazed this methodology to high heaven. What was supposed to be a quick and easy way to centralize my types became one of the most painful development and deployment experiences I’ve had to date. I wasted hours of productivity just trying to make minor type updates reflect correctly across my projects. I had to roll back deployments multiple times. I introduced bugs not from the business logic, but from version mismatches, installation errors, or failed builds because the damn package wouldn’t resolve.

This blog post is a documentation of my failures, mishaps, and bad decisions. And it serves as a firm warning to anyone who's thinking about doing the same thing. Just don’t.

The Setup Looked Pretty Legit

The plan seemed solid at first. I’d create a separate repo with a src/ folder, set up barrel exports, build with tsc, publish the resulting files to GitHub Packages, then install it from my frontend and backend projects. I even used Zod so I could export both runtime validators and static types in one go.

I had the whole thing working locally. GitHub Actions handled the publishing pipeline. I versioned the package semantically. All I had to do was create a .npmrc with my package credentials to the consuming projects. It felt clean and scalable.

The Problems Hit Fast and Hard

Here’s where it all started to fall apart.

Authentication Overhead

GitHub Packages requires a personal access token for every install. That meant setting up a GITHUB_TOKEN environment variable in every local dev machine, in every CI pipeline, and in every deployment environment. That alone introduced some friction. If I forgot to set the token in even one place, my build would fail with a mysterious 401 or 404 error.

Every time I switched between VPS deployment to Cloudflare Workers to Vercel to other places I usually turn to, I had to go through the token setup all over again. And npm doesn’t make this process elegant. At one point, I was hardcoding the token just to make it work, not caring when Git started tracking the file. (This project is not open source, obviously.)

Slow Feedback Loop

Here’s what the workflow looked like just to make a single change in a type reflect across my backend and frontend:

  1. Make the change in the shared types repo.
  2. Bump the version.
  3. Run the build.
  4. Publish the package.
  5. Go to the consuming projects.
  6. Update the dependency version manually.
  7. Clear node_modules and lock files because npm’s cache wouldn’t pick up the update half the time.
  8. Reinstall everything.
  9. Hope nothing broke.

Sometimes it worked. Other times I was left debugging version mismatches because npm silently installed an outdated version.

I didn’t just feel like I was shipping code. I felt like I was performing a deployment ritual.

CI/CD Nightmares

All of this complexity magnified when I tried to deploy.

Some CI environments wouldn’t install the private package because I forgot to pass the token. Some staging builds failed silently because the token didn’t have the right scopes. The production deploy failed one time because I accidentally referenced a type that hadn’t made it into the published build due to a stale cache.

The worst part was that I wasn’t even working on business logic during these failures. I was just trying to get my own types to install properly.

If I Looked Harder, I Would Have Seen the Red Flags

If you dig through the internet, you’ll find people trying to solve this same problem using local npm link, custom package proxies, or monorepo tooling like Nx or Turbo. That’s because this approach, sharing types via a private npm package, looks great in a diagram but falls apart in real-world use cases.

If your types are changing frequently, the overhead of maintaining and syncing versions is unbearable. If you’re working across multiple environments or machines, the need to configure authentication everywhere turns into a massive time sink. And if you’re deploying to environments without direct access to private registries (like edge functions or third-party CI/CD platforms), you’re basically dead in the water.

There Are Better Options

If you truly need to share types across frontend and backend, there are options that won’t cause you to hate life.

Use tRPC

If you’re using TypeScript and can architect your API with function calls instead of REST endpoints, just use tRPC. It gives you end-to-end type safety with no duplication and zero need for separate type packages. One source of truth. No publishing steps. No tokens. It just works.

Use a Monorepo

Put your frontend, backend, and shared code in one repo. Use pnpm or bun workspaces, turborepo, or even just ../shared imports. It’s simple, fast, and avoids the need for separate build pipelines. You don’t need to publish anything to get updated types.

Final Thoughts

I went through the trouble of setting up a full GitHub Package publishing pipeline. I learned how to write a GitHub Actions workflow that builds and deploys a scoped package. I versioned things properly. And yet the moment I actually started using it, it became a huge liability.

I don’t say this lightly: avoid sharing types via private npm packages unless you have a large team, a DevOps department, and the patience of a saint.

For solo developers or small teams, this method is a trap disguised as good architecture. Don’t fall for it. Use tRPC. Use turborepo. Hell, copy and paste the types if you have to. Just don’t build a pipeline that adds more friction than value.

The time I lost trying to debug registry errors, mismatched versions, broken builds, and broken deployments? I could’ve just used local imports and saved myself a week’s worth of pain.

Never again.

Let's take this to your inbox.

Sign up to my newsletter!

[email protected] Subscribe