
This series is an opinionated take on how to approach building a library—whether it ships inside your organization or as open source. Part 1 focuses on specification: how you decide what to build before you drown in implementation details.
Where do I start?
Know your consumer(s)
Before you write components, services, or utilities, invest time in who will consume the package: other teams, external contributors, or future you. Ask:
- What runtime and bundler do they use?
- Do they need tree-shaking and ESM/CJS dual builds?
- What is their skill level with the ecosystem (e.g. Angular, Node)?
That context shapes API surface, peer dependencies, and documentation tone.
Document how you would like to use it
If you are building a library—org-internal or community-facing—you probably already understand the problem space. A practical accelerator is README-driven development: write the usage document you wish existed before the code catches up.
Golden rule: the first README does not need every edge case. It needs a credible happy path that a reader can copy, paste, and adapt.
Example — Slothy Client (fictional)
Slothy Client is a small client that makes task storage and retrieval easier. It keeps pending tasks and lets you work through them.
Installation
npm install @slothy/client --save
Setup
import { createSlothyClient, SlothyClientConfig } from '@slothy/core';
const config: SlothyClientConfig = {
storage: 'in_memory',
debug: false,
};
const client = await createSlothyClient(config);
Integrations — Angular
npm install @slothy/angular --save
Then import SlothyClientModule.forRoot(...) in your AppModule (see the Angular package README for full steps).
SlothyClientConfig
| Property | Type | Description |
|---|---|---|
storage |
'in_memory' | 'indexed_db' |
Where tasks are persisted |
debug |
boolean |
Enables verbose logging |
That single page already forces you to name concepts (SlothyClient, createSlothyClient, config shape) that will become your public contract.
MVP before overbuilding
In the first iteration, stay focused on the core problem. Two common traps:
- Over-complete specs — even if you know the domain deeply, resist documenting every future feature. Capture the smallest behavior that proves value.
- Bloated or vague contracts — prefer small, composable interfaces you can extend later, and document only what v0 truly guarantees.
Layered architecture
Keep your domain isolated
Frameworks and UI libraries change; your domain model and invariants should not leak into every consumer. A typical layout for @slothy/core:
.
└── lib/
├── domain/
│ └── ...
├── core/
│ └── ...
├── utils/
│ └── ...
└── index.ts
index.ts must export only what you intend to support as public API. Everything else stays internal or behind explicit entry points (@slothy/core/testing, etc.).
export { createSlothyClient, SlothyClient, SlothyClientConfig } from './domain';
Consumer code
import { createSlothyClient, SlothyClientConfig } from '@slothy/core';
const config: SlothyClientConfig = {
storage: 'in_memory',
debug: true,
};
const client = await createSlothyClient(config);
client.doSomething();
Make it as pluggable as you can
Pluggability here means: stable core, swappable edges.
- Prefer functions and factories (
createSlothyClient) over hidden singletons. - Expose narrow interfaces for storage, logging, and transport so teams can replace them without forking the library.
- Use peer dependencies for frameworks (Angular, React) so consumers control versions and avoid duplicate installs.
Document one blessed integration path per major ecosystem; keep optional adapters in separate packages.
Write multiple clients/adapters for your domain
If consumers must integrate with Angular, CLI tools, or other stacks, ship thin adapter packages that wrap the core and translate framework-specific configuration into your domain config.
Example layout for @slothy/angular:
.
└── lib/
├── utils/
│ └── ...
├── ng-slothy.config.ts
├── ng-slothy-client.module.ts
└── index.ts
ng-slothy.config.ts:
import { SlothyClient, SlothyConfig } from '@slothy/core';
export interface NgSlothyClientModuleConfig extends SlothyConfig {}
export class NgSlothyClient extends SlothyClient {}
export function mapToSlothyConfig(
ngConfig: NgSlothyClientModuleConfig
): SlothyConfig {
const config: SlothyConfig = { ...ngConfig };
return config;
}
ng-slothy.tokens.ts (simplified):
import { InjectionToken } from '@angular/core';
import type { SlothyClient, SlothyConfig } from '@slothy/core';
export const SLOTHY_CONFIG = new InjectionToken<SlothyConfig>('SLOTHY_CONFIG');
export const SLOTHY_CLIENT = new InjectionToken<SlothyClient>('SLOTHY_CLIENT');
ng-slothy-client.module.ts:
import { NgModule, ModuleWithProviders } from '@angular/core';
import { createSlothyClient } from '@slothy/core';
import {
NgSlothyClientModuleConfig,
NgSlothyClient,
mapToSlothyConfig,
} from './ng-slothy.config';
import { SLOTHY_CONFIG, SLOTHY_CLIENT } from './ng-slothy.tokens';
@NgModule()
export class SlothyClientModule {
static forRoot(
config?: NgSlothyClientModuleConfig
): ModuleWithProviders<SlothyClientModule> {
const slothyConfig = mapToSlothyConfig(config ?? {});
return {
ngModule: SlothyClientModule,
providers: [
{ provide: SLOTHY_CONFIG, useValue: slothyConfig },
{
provide: SLOTHY_CLIENT,
useFactory: () => createSlothyClient(slothyConfig),
},
{ provide: NgSlothyClient, useExisting: SLOTHY_CLIENT },
],
};
}
}
Consumer code
import { SlothyClientModule } from '@slothy/angular';
const SLOTHY_MODULE_CONFIG: NgSlothyClientModuleConfig = {
param: 'my-awesome-slothy',
debug: false,
};
@NgModule({
imports: [
SlothyClientModule.forRoot(SLOTHY_MODULE_CONFIG),
],
})
export class AppModule {}
The Angular package maps framework configuration to core configuration; it does not reimplement domain logic.
What “specification” means in Part 1
- A README-shaped contract you can iterate on with users.
- An MVP boundary that avoids premature completeness.
- A layered package map: core domain first, adapters second.
Continue in Part 2 — Versioning, changelog & testing for versioning, changelog discipline, and testing strategies that keep that contract honest over time, then Part 3 — Contributors & documentation for CONTRIBUTING.md, RFCs, and API documentation.
Tags: open source, library, TypeScript, Angular