The interface is the decision
Most of what a module will cost you is settled by its interface, before a line of its body is written.
· updated
You can rewrite a module's body on a quiet afternoon. You cannot rewrite its interface without touching every caller, and the callers are the part you do not control. So the interface is where the decision actually gets made, and it gets made early, usually before anyone has seen the problem clearly enough to make it well.
What follows is what I do about that.
The first move is to ask what a deep module would look like here — one that hides enough behaviour that the caller can forget the rest.
Three questions, in order
- What does the caller already know?
- What would it have to learn to use this?
- Is the difference worth the module existing?
The third question is the one that kills modules, and it should. A wrapper that renames its dependency has a cost and no depth — and a type alias like type Id = string is the same trade in miniature.
The signature carries the whole argument:
interface ContentGraph {
nodeAt(route: string): Node | null
incomingReferences(termSlug: string): Node[]
routes(): Route[]
}Three methods, and everything downstream is a pure function of them. The slug derivation, the canonical construction, the pagination, the consistency assertions — none of that appears here, which is the point.
A narrow interface is a promise you can keep.
What this rules out
- Configuration objects that grow a field per caller
- Optional parameters that change the return type
- Anything named `helpers`

The vocabulary here is John Ousterhout's, and the book is short enough to read in a sitting.