DEV Community

Cover image for I built a TypeScript client for Ceph Object Storage because the only npm package was 7 years old. Here's What I Learned
Himanshu Kumar
Himanshu Kumar

Posted on

I built a TypeScript client for Ceph Object Storage because the only npm package was 7 years old. Here's What I Learned

Last month I went looking for a Node.js client to manage users and buckets on our Ceph RADOS Gateway. Found one package on npm — rgw-admin-client. Last published in 2019. No TypeScript. No ESM. No maintenance.

So I built my own.

What even is Ceph RGW?

If you've worked with Kubernetes storage, you've probably bumped into Ceph. It's the storage backend behind Rook-Ceph and Red Hat OpenShift Data Foundation. The RADOS Gateway (RGW) is its S3-compatible object storage layer.

RGW has an Admin API to manage users, buckets, quotas, rate limits, and usage stats. But to use it from Node.js, you either:

Shell out to the radosgw-admin CLI inside a Ceph toolbox pod
Write raw HTTP requests with AWS SigV4 signing by hand
Use that 7-year-old unmaintained package and hope for the best

None of those felt great.

What I built

npm install radosgw-admin

import { RadosGWAdminClient } from 'radosgw-admin';

const rgw = new RadosGWAdminClient({
  host: 'http://ceph-rgw.example.com',
  port: 8080,
  accessKey: 'ADMIN_ACCESS_KEY',
  secretKey: 'ADMIN_SECRET_KEY',
});

// create a user
const user = await rgw.users.create({
  uid: 'alice',
  displayName: 'Alice',
  email: 'alice@example.com',
});

// set a 10GB quota (yes, you can just write "10G")
await rgw.quota.setUserQuota({
  uid: 'alice',
  maxSize: '10G',
  maxObjects: 50000,
});

// list all buckets
const buckets = await rgw.buckets.list();
Enter fullscreen mode Exit fullscreen mode

8 modules. 39 methods. Zero runtime dependencies.

Why zero dependencies?
The RGW Admin API uses AWS Signature V4 for auth. Most people would reach for aws-sdk or @aws-sdk/signature-v4 for that. I didn't want to pull in a massive dependency tree for one signing function.

So I wrote the SigV4 implementation using just node:crypto. It's about 80 lines. The whole package has zero production dependencies — your node_modules stays clean.

Things I actually cared about while building this
TypeScript that helps, not annoys
The whole codebase runs with strict: true, noImplicitAny, exactOptionalPropertyTypes, and noUncheckedIndexedAccess. No any anywhere.

What this means for you — autocomplete works properly, types match what the API actually returns, and you catch mistakes before running anything.

snake_case in, camelCase out
The RGW API returns JSON with snake_case keys. Every JavaScript developer expects camelCase. The client transforms both directions automatically:

// you write camelCase
await rgw.users.create({ displayName: 'Alice', maxBuckets: 10 });

// the API receives: display-name=Alice&max-buckets=10

// the API returns: { "user_id": "alice", "display_name": "Alice" }

// you get back camelCase
user.userId;      // "alice"
user.displayName; // "Alice"
Enter fullscreen mode Exit fullscreen mode

You never think about it.

Errors you can actually catch
Instead of generic "Request failed with status 404", you get typed errors:

import { RGWNotFoundError, RGWAuthError } from 'radosgw-admin';

try {
  await rgw.users.get('nonexistent');
} catch (error) {
  if (error instanceof RGWNotFoundError) {
    // user doesn't exist — handle it
  }
  if (error instanceof RGWAuthError) {
    // bad credentials — log and alert
  }
}
Enter fullscreen mode Exit fullscreen mode

There's RGWNotFoundError, RGWAuthError, RGWConflictError, RGWValidationError, and a base RGWError. Validation errors are thrown before any HTTP call — so you don't waste a round trip on bad input.

Size strings that make sense
Quota methods accept human-readable sizes:

await rgw.quota.setUserQuota({ uid: 'alice', maxSize: '10G' });
// internally converts to 10737418240 bytes
Enter fullscreen mode Exit fullscreen mode

You can write '500M', '1T', '1.5G' — whatever makes sense. Or pass raw bytes if you prefer.

The hard parts
SigV4 signing — AWS Signature V4 has a very specific signing process. Canonical request, string to sign, signing key derived from date + region + service. Getting the exact byte-level match right took a few days of reading the AWS docs and comparing against known-good signatures.

Ceph's response formats — Some endpoints return a JSON array on success but an XML error on failure. Some return empty body for success. The GET /bucket endpoint changes its response shape depending on whether you pass max-entries or not. Each of these needed special handling.

Dual ESM + CJS — Shipping a package that works with both import and require() in 2026 is still annoying. I used tsup to build both formats with correct exports mapping in package.json. Validated with publint and @arethetypeswrong/cli in CI.

What's in the box

Module What it does
Users Create, get, modify, delete, list, suspend, enable, stats
Keys Generate and revoke S3/Swift access keys
Subusers Create, modify, remove Swift subusers
Buckets List, info, delete, transfer ownership, verify index
Quota Get/set user and bucket quotas with size strings
Rate Limits Per-user, per-bucket, and global rate limiting
Usage Query bandwidth/ops reports, trim old logs
Info Cluster FSID and storage backend info

Every method has JSDoc with @example blocks, so your editor shows you exactly how to use it.

If you're running Rook-Ceph
Port-forward the RGW service and point the client at it:

kubectl port-forward svc/rook-ceph-rgw-my-store 8080:80 -n rook-ceph

const rgw = new RadosGWAdminClient({
  host: 'http://localhost',
  port: 8080,
  accessKey: '...', // from rook secret
  secretKey: '...',
});
Enter fullscreen mode Exit fullscreen mode

Get the admin credentials:

kubectl get secret rook-ceph-dashboard-admin-gateway -n rook-ceph \
  -o jsonpath='{.data.accessKey}' | base64 -d
Enter fullscreen mode Exit fullscreen mode

Numbers
280 tests passing
CI runs on Node 18, 20, and 22
90%+ code coverage
npm provenance with trusted publisher
Dual ESM + CJS with full type declarations

What's next
I'm planning to add multi-site/zone management and IAM role support in future versions. Open to suggestions — if you use the RGW Admin API and there's something missing, let me know.

Links:

npm: https://www.npmjs.com/package/radosgw-admin
GitHub: https://github.com/nycanshu/radosgw-admin
Docs: https://nycanshu.github.io/radosgw-admin

If you find it useful, a star on GitHub helps others find it too.

Top comments (0)