Module Layout (Opinionated)¶
The shared platform library should feel like a small standard library for the repo: stable, boring, and predictable.
Recommended Top-Level Modules¶
Group by cross-cutting concern, not by “utils”:
auth/: JWT verification, key providers, auth header helpersconfig/: env decoding/validation, typed config objectserrors/: typed errors + helpers to normalizeunknownresult/:Result<T, E>utilities and constructorsgrpc/: server/client helpers, handler wrappers, interceptors, service registrationhttp/: client/server helpers (lightweight; avoid frameworks)lifecycle/: start/stop/dispose patterns; shutdown coordinationobservability/: logger mixins (traceId/spanId), metrics primitives, tracing helpersretry/: backoff helpers, retry policies (paired with idempotency guidance)proxy/: safe wrappers around clients (timeouts, cancellation, logging)redis//pg/: lightweight helpers and conventions; avoid hiding business queries here
Public API Surface¶
- Keep a stable
src/index.tsthat re-exports the “supported” surface. - Prefer explicit exports over
export *when the surface is large. - Avoid barrel exports that accidentally import heavy dependencies into every consumer.
Dependency Direction (Avoid Cycles)¶
Try to keep a leaf-to-root direction like:
types/,result/,errors/are leaves.config/and low-level helpers depend only on leaves.observability/depends on leaves (and maybeconfig/), but shouldn’t depend ongrpc/.grpc/andhttp/depend on leaves +observability/(for consistent instrumentation).lifecycle/can be a leaf-ish utility used by server/client constructors.
If you hit a cycle:
- Extract shared types to a leaf module.
- Invert the dependency (pass a callback instead of importing).
- Move wiring to a composition root in the consuming service.
“Two Consumers” Rule¶
Don’t add a module until:
- it has 2+ real consumers, or
- it is a prerequisite for a planned second consumer in the same PR series.
This prevents the shared library from becoming a dumping ground for one-off code.