Writing

One library, many opinions

October 21, 2026

LIBRARY PUBLIC API CONSUMERS button forms button-internals date-utils not exported public-api.ts app one app two
Only what crosses public-api.ts is a promise. Everything to the left of it can change tomorrow.

It started with a date formatter. One function, maybe eight lines, copied into three Angular apps because it was faster than setting anything up. A year later there were three date formatters. One handled time zones, one had a bug the other two had fixed, and one had grown an optional argument nobody could explain. The shared library was supposed to end that. It did, and then it handed me a different set of problems I hadn’t known to worry about.

The first surprise is that you can’t just publish an Angular folder. The templates and decorators need compiling into something a consumer’s build can understand, and that consumer may be on a slightly different Angular version than you. That’s what ng-packagr is for. It produces the Angular Package Format, which is a fancy way of saying “the shape the CLI expects,” and it compiles partially, leaving the final step to whoever installs it. I spent a while treating it as an annoying extra build. It’s the whole reason the thing works across apps at all.

The real product, it turned out, wasn’t the components. It was one file called public-api.ts. Every line in it is a promise. Export a component and its inputs are now something you can never rename without a version bump and a conversation. Export a helper “just in case” and someone will build on it by Thursday. The best decision I made was being stingy there. A lot of the library is deliberately unreachable, and that’s what lets me rewrite it on a quiet afternoon without anyone noticing.

Then we hit the bundle size. Importing one button pulled in the forms module, the date helpers and a chunk of stuff nobody asked for, because it was all one entry point and tree-shaking can only do so much with Angular metadata. The fix was secondary entry points: @scope/ui/buttons, @scope/ui/forms, each its own little package with its own ng-package.json. It’s more boilerplate than I’d like. It also means an app that wants a button gets a button and nothing else, and the boundaries between pieces are now enforced by the import path instead of by good intentions.

The afternoon I lost to peer dependencies deserves its own paragraph. Angular was listed as a plain dependency. One of the apps installed the library and got a second copy of Angular, so a service provided in the app was being looked up by a library that had its own idea of what the injection token was. The error said “no provider,” and the provider was right there, and I read that line a lot. Angular, RxJS, anything the consumer already has, goes in peerDependencies with an honest version range. Nothing else taught me the difference between the two fields quite as thoroughly.

What I didn’t expect was how much the newer Angular made this easier. A standalone component carries its own imports, so there’s no module to export alongside it and no module to forget. Signal inputs give each component a flat, readable surface, and computed() keeps the derived state inside where it belongs. The library got smaller on the outside while doing more on the inside, which is exactly the direction you want a shared thing to grow.

The job of a library, I’ve decided, is to be boring. Nobody wants a clever button. They want the same button they had yesterday, with the fix they asked for, and a changelog they can read in thirty seconds. A library is a contract with people who will never open the source, including you in six months. Fewer exports, honest versions, and one date formatter. That last one is still only eight lines.

— Adi

← Back to blog