Package foundations
Decide what the package owns
Draw a narrow public boundary before adding metadata, builds, or publishing automation.
A package is a versioned contract. Someone installs it, imports it, and expects the documented behavior to stay stable. That someone might be you in another repo six months from now.
We will use a small utility called slugify-title as the running example. It turns blog post titles into URL slugs:
import { slugifyTitle } from 'slugify-title'
slugifyTitle('Hello World!')
// 'hello-world'
That is the whole job. The package owns title normalization and slug creation. It does not own routing, database lookups, or UI state. Those belong in the app that calls it.
Draw the boundary before you extract code
A common mistake is to move every helper from a folder into one npm package because they sit together in git. The folder layout is not the public API. Consumers only see what you export and document.
Write a short contract on one page:
- Purpose: convert a string title into a lowercase hyphenated slug
- Inputs: a non-empty string; optional
{ strict: true }to reject unsafe characters - Outputs: a slug string, or a thrown error when input is invalid
- Runtime: Node 20+ and modern browsers (no DOM APIs)
- Non-goals: fetching titles, checking uniqueness in a database, SEO scoring
If you cannot list three real usage examples from that contract, the boundary is still too fuzzy.
Keep the first release small
My advice is to ship the smallest surface you can support forever. Every exported function becomes a promise. Removing one later means a major version bump and migration work for everyone who installed you.
For slugify-title, v1 might export only slugifyTitle. A stricter variant can live behind slugify-title/strict in a later minor once the main path is stable.
Try this on your own project: pick one function you copy between repos today. Write its contract, three call examples, and two things it will never do. Only then create the package directory.
Lesson completed