Skip to content
Go back

Building Your First npm Package — What Nobody Tells You About Shipping Production TypeScript

8 min read

I built and published radosgw-admin — a TypeScript SDK for managing Ceph Object Storage clusters. Zero dependencies, AWS Signature V4 auth, dual ESM/CJS, 280+ unit tests.

This post isn’t about the package itself. It’s about the stuff I learned that I wish someone had told me before I started.

The code is maybe 30% of the work

I had the core API client working in a few days. Request signing, user management, bucket operations — all functional.

Then reality hit. TypeScript declarations that work for both ESM and CJS consumers. CI pipelines that test across Node 18, 20, and 22. npm provenance so people can verify the package wasn’t tampered with. A documentation website. Error messages that actually help someone debug at 2am. A CHANGELOG. A CONTRIBUTING guide. A security policy.

The “last mile” from working code to publishable package took longer than writing the code itself. If you’re planning to publish something, double whatever timeline you have in your head.

Zero dependencies is a feature you earn

The RGW Admin API uses AWS Signature V4 for authentication. The obvious move is npm install aws-sdk and move on. I wrote the signing myself — about 80 lines using node:crypto.

Why? Because zero production dependencies means:

It took extra effort. But it’s a feature I can put on the box and mean it. When someone evaluates your package, “zero dependencies” is a trust signal.

Project structure will save you or sink you

Here’s something I didn’t appreciate early enough: how you organize your code determines how maintainable it is six months later.

I started with everything in a few big files. It worked. Then I needed to add bucket quotas, and I had to read through 400 lines to figure out where the change should go.

I refactored into a structure where each domain has its own module:

src/
  client/        # HTTP client, request signing
  resources/     # user.ts, bucket.ts, quota.ts, usage.ts
  types/         # All TypeScript interfaces
  errors/        # Custom error classes
  index.ts       # Public API surface

Every new feature now has an obvious home. Every bug has a predictable location. If someone opens a PR, they know exactly which file to touch.

The lesson: invest in structure early. Not a “clean architecture” astronaut design — just obvious, boring folder organization that a stranger could navigate in 30 seconds.

Write errors like you’re debugging at 2am

Early versions of my SDK threw generic errors: Error: Request failed. Completely useless.

Now every error includes:

// Bad
throw new Error("Request failed");

// Good
throw new RGWAdminError(
  `Failed to create user "${uid}": ${response.status} ${errorCode}. ` +
  `Check that your access key has admin caps for "users=write".`
);

When your library throws an error, the developer using it shouldn’t have to read your source code to understand what happened. The error message is your documentation at the point of failure.

Custom error classes also let consumers catch specific failure types:

try {
  await rgw.createUser({ uid: "test" });
} catch (e) {
  if (e instanceof RGWAdminError) {
    // handle API-specific error
  }
}

This one change dramatically reduced the “how do I use this?” questions.

Test against real services, not mocks

This one burned me and I’ll never forget it.

I had 100% mock coverage. Every test passed. I was confident. Then someone tried it against an actual Ceph cluster and half the operations failed — because the real API returns slightly different response shapes than the docs describe.

Mocks test your assumptions. Integration tests test reality.

I now run two test suites:

# Unit tests (fast, every PR)
npm test

# Integration tests (real Ceph cluster in Docker)
npm run test:integration

The integration tests have caught bugs that unit tests never would have found:

If your package talks to an external service, mock tests alone will betray you. Budget time for integration tests against the real thing, even if it’s just a Docker container.

TypeScript strict mode is pain that pays off

I turned on every strict flag TypeScript has: noImplicitAny, exactOptionalPropertyTypes, noUncheckedIndexedAccess, strict: true.

Writing code was noticeably slower. The compiler complained about things that “obviously work.” I almost turned some flags off three different times.

But when I used the package from a consumer’s perspective — autocomplete worked perfectly, types matched real API responses, and mistakes showed up before running anything.

Your users don’t see your source code. They see autocomplete suggestions and type errors. Strict mode is how you make that experience good.

Nobody finds your package unless you tell them

There are 2+ million packages on npm. Publishing and hoping for discovery doesn’t work.

I actively shared on:

The dev.to article alone drove more traffic than the npm listing ever did organically. If you’re building open source, marketing is not optional. Budget real time for it.

Ship v0.1.0, not v1.0.0

I wanted to keep polishing. Fix one more edge case. Add one more feature. Write one more test.

But the feedback from real users is worth more than another week of solo development.

v0.1.0 says “this works, try it, tell me what’s missing.”

v1.0.0 says “this API is stable and I won’t break it.”

I wasn’t ready to make that promise. And the feedback I got from early adopters shaped the API in ways I never would have thought of alone.

Document the why, not just the how

My first docs were pure reference — function signatures, parameter types, return values. Technically complete, practically useless for someone getting started.

What actually helped users:

The documentation site took real effort to build, but it’s the single biggest driver of adoption. People judge your package by the docs before they ever read a line of source code.

Keep a changelog — your future self will thank you

Every release gets a CHANGELOG entry. Not just “bug fixes” — actual descriptions of what changed and why.

## [0.4.0] - 2026-03-10
### Added
- Rate limit management (get/set per-user rate limits)
- Usage trimming with date range support

### Fixed
- Bucket stats returning null for empty buckets (Ceph Quincy)

### Changed
- Error messages now include the attempted operation name

Six months from now, when someone asks “when did you add rate limit support?” or “did you fix that null bucket stats thing?” — the changelog answers instantly. It’s also how you write release notes without having to git log through 50 commits.

The real takeaway

Building a package that works is step one. Building a package that people want to use requires:

The code is the easy part. Everything around it is what separates a side project from a real tool.


If you work with Ceph, Rook-Ceph, or OpenShift Data Foundation and need to manage RGW users, buckets, or quotas from Node.js:


Share this post on:

Related Posts


Next Post
Hello World — Why I Built This Blog