Skip to main content

Module layout

The folder tree of an API and of one module, what goes in each folder, and how files are named. Read it when you add a module or wonder where a file belongs.

How it works​

The API​

src/
server.ts # create Fastify, global hooks, register each module
config/
env.ts # secrets from the environment, Zod-validated
settings.ts # non-secret settings with defaults
shared/
db/ # pool, withTransaction, migration runner
errors/ # AppError, codes, HTTP mapping
result.ts # Result<T, E>
auth/ # verify token, guards: requireUser, requireStaff
logger.ts
infrastructure/ # clients more than one module uses
modules/
<feature>/

One module​

modules/orders/
index.ts # public functions other modules may call, and register()
api/
orders.routes.ts # REST, or orders.resolvers.ts for GraphQL
orders.schemas.ts # Zod schemas for input and output
use-cases/
create-order.ts
cancel-order.ts
repositories/
orders.repo.ts
ports/ # only if the module calls an outside service
payments.port.ts
adapters/
stripe-payments.ts
db/
schema.ts # this module's tables
README.md # what the module owns, its tables, its events

Naming​

ThingNameNot
Module folderThe domain, singular or plural, kebab-case: orders, prayer-requestsProject phases: slice-8, phase2
Use-case fileThe action: cancel-order.tsorder-service.ts with 40 functions
Route file<module>.routes.ts or <module>.resolvers.ts. Split by sub-area when it passes about 300 lines: orders-admin.routes.tsslice-3-resolvers.ts
Repository<table group>.repo.tsdb.ts
TestNext to the file: cancel-order.test.tsA distant tests/ mirror tree, except for end-to-end flows

Rules​

Rule

index.ts stays small: it exports public functions and a register() that mounts routes. If wiring passes about 200 lines, split it by sub-area. A 1,500-line composition file is a sign the module is two modules.

Rule

Name files after the domain, never after the sprint or the delivery slice. Six months later nobody remembers what "slice 8e" was.

Rule

Every module has a README listing what it owns, its tables and the events it publishes. Keep it true; a README that lists folders that no longer exist is worse than none.

Mistakes we made​

  • Resolver files named slice-3 to slice-11, including slice-8a to slice-8f. Nobody could find a resolver without searching.
  • A settings file grew to over 4,000 lines. Split settings by module, next to the module, and compose them.
  • Docs listed modules that did not exist and missed ones that did, after renames. Update the README in the same pull request as the rename.
  • The package name kept the old project name after the repo was renamed.

Checklist​

  • Module folder named after the domain.
  • One use-case per file, named after the action.
  • index.ts small, exports only public functions.
  • Module README current.