← Back to blog
5 min readArchitecture

How to write an Organization/Open Source library (Opinionated) | Part 2 — Versioning, changelog & testing

SemVer, changelogs, release discipline, and a testing strategy that protects your library contract—continuing the Slothy example from Part 1.

This is Part 2 of the series. If you have not read it yet, start with Part 1 — Specification: README-first workflows, MVP contracts, and layered packages (@slothy/core, @slothy/angular).

Here we focus on what keeps that contract honest over time: versioning, changelog discipline, and testing.


Versioning — SemVer with intent

Semantic versioning (MAJOR.MINOR.PATCH) is a communication tool. Consumers read a version bump as a signal:

Bump Typical meaning
MAJOR Breaking change to the supported public API (or behavior users relied on).
MINOR New backward-compatible features (new optional config, new export).
PATCH Bug fixes that preserve the documented contract.

What counts as “breaking”?

Be stricter than “the build still passes.” Breaking changes include:

  • Renaming or removing exported symbols.
  • Tightening types in a way that rejects previously valid inputs.
  • Changing default behavior when the old behavior was observable and undocumented—but consumers will still depend on it.

If you must change behavior that people rely on, either document the old behavior as unsupported in a MINOR with a deprecation path, or ship a MAJOR with a migration note.

The 0.x era

While the API is still fluid, 0.y.z signals instability: MINOR may include breaking changes until you commit to 1.0.0. After 1.0.0, treat MAJOR bumps as a big deal: migration guide, codemods if possible, and a clear story in the changelog.

Aligning multiple packages (@slothy/*)

If you run a monorepo with @slothy/core and @slothy/angular:

  • Option A — independent versions: each package has its own semver. Document which adapter versions are compatible with which core range (peer dependency + release notes).
  • Option B — lockstep: one version number for all packages (simpler for consumers, heavier for maintainers).

Pick one strategy and document it in the root README. Avoid “silent” peer drift.


Changelog — one source of truth

Automated git logs are noisy. A human changelog (see Keep a Changelog) helps consumers answer: Should I upgrade today?

Recommended sections:

  • Added — new APIs, features.
  • Changed — behavior changes that are not breaking.
  • Deprecated — what will be removed, and when.
  • Removed — actually deleted in this release.
  • Fixed — bug fixes.
  • Security — advisories.

Link changes to the public API

Instead of “fixed internal refactors,” prefer:

Fixed: createSlothyClient no longer throws when storage is indexed_db and the store is empty (issue #42).

That ties the note to what users do, not how you refactored folders.

Migration guides for MAJOR

For every MAJOR, add MIGRATION.md (or a section in the changelog) with:

  1. Before / after code snippets.
  2. Why the change happened (one paragraph).
  3. Mechanical steps (search-replace, renamed exports).

Releases — predictable rhythm

Whether you publish to npm (public or private) or to an internal registry:

  1. Tag in git (v2.0.0) matching the published version.
  2. CI runs on the tag: tests, build, optional bundle-size check.
  3. Publish from a clean tree (many teams use npm publish / pnpm publish in CI only).

For organizations, pin who can publish and which branch may release (e.g. main only).

Deprecations

Use TypeScript @deprecated JSDoc, runtime console.warn (sparingly), and changelog entries with a removal target (“removed in v3”).


Testing — protect the contract, not the folders

1. Test the public API first

Your tests should import @slothy/core the same way consumers do (from the built entry, not deep paths into src/lib/domain/... unless those paths are officially supported).

// slothy-core.public-api.spec.ts
import { createSlothyClient } from '@slothy/core';

it('creates a client with in_memory storage', async () => {
  const client = await createSlothyClient({ storage: 'in_memory', debug: false });
  expect(client).toBeDefined();
});

If refactoring internals breaks only internal tests, you moved too much logic away from the supported surface.

2. Contract tests for adapters

For @slothy/angular, assert that forRoot provides a client that behaves like core:

  • Same task operations succeed/fail under the same config.
  • Tokens resolve without duplicate SlothyClient instances in typical setups.

3. Avoid testing implementation details

Do not assert private method names or internal module graphs unless they are part of your stability promise.

4. Matrix what matters

At minimum:

  • Unit tests for pure domain logic.
  • Integration tests for storage backends (in_memory vs indexed_db in a headless environment when feasible).

For Angular adapters, use TestBed smoke tests that imports compile and forRoot registers providers.


CI as a gate

A practical baseline:

Check Why
Lint + format Keeps API exports and docs consistent.
tsc --noEmit / build Catches broken types before publish.
Tests See above.
Pack dry-run Ensures package.json files / exports ship what you think.

Optional but valuable: bundle size limits on @slothy/core to catch accidental heavy dependencies.


How Part 2 connects to Part 1

Part 1 Part 2
README as contract Changelog explains how that contract evolves
MVP boundaries SemVer encodes what “safe upgrade” means
Layered packages Version alignment + adapter testing

What’s next?

Continue with Part 3 — Contributors & documentation for CONTRIBUTING.md, issue templates, RFCs, and API docs (TSDoc / generated reference).


Tags: open source, library, TypeScript, Angular, semver, testing