| name | threadline-api-endpoint |
| description | Use when adding, changing, or removing an HTTP route in apps/api — including anything that touches the Repository interface, the OpenAPI specification, or an authorization decision. Enforces the six-step order that keeps routes, persistence, policy, spec, tests, and docs in sync. |
Adding an API endpoint
An endpoint in apps/api is six artefacts, not one. Skipping any of them produces a change that passes locally and fails in review, in CI, or — worst — in production against a database the test suite never exercises.
Do them in this order. The order matters: each step's output is the next step's input, and doing them backwards produces a route whose spec and tests were written against an interface that then changed.
1. Domain type — apps/api/src/domain.ts
Only if the shape is genuinely new. Reuse the existing vocabulary where one fits — User, Organization, Membership, Room, RoomMembership, RoomEvent, CalendarEvent, PersonalAccessToken, Scope.
If the endpoint needs a new scope, add it to the scopes array. Scope is derived from that array, so nothing else needs editing — and a scope invented inline in a route rather than added here will not typecheck.
2. Persistence — apps/api/src/repository.ts
Three edits, always together:
- Add the method to the
Repository interface.
- Implement it in
MemoryRepository.
- Implement it in
MongoRepository.
Implementing only one is the single most common way to break this codebase, and the failure is asymmetric: miss MemoryRepository and every test fails immediately; miss MongoRepository and everything passes until deployment. TypeScript catches a missing interface implementation, so let it — add the interface method first, then let the two compile errors tell you what to write.
Never import mongodb outside this file. That is ADR 0003.
3. Route — apps/api/src/application.ts
Place it next to its siblings — routes are grouped by resource, and a new one dropped at the end of the file is a review comment.
Every route does these in order:
- Authenticate. Reuse the existing session / PAT middleware; do not re-derive identity.
- Validate the request body, params, and query with Zod. An unvalidated body reaching the repository is a defect regardless of whether it currently causes a bug.
- Load the resources the decision needs — the
Membership, the Room, the RoomMembership.
- Decide by calling
policy.ts:
canOrganization(membership, action) — read, create_room, manage_members, schedule
canRoom(membership, room, roomMembership, action) — read, join_live, write, manage
canInviteToOrganization(membership, organization) — join-code visibility and rotation
- Act, then respond.
Never infer access from an id alone. Possessing a room id is not evidence of membership in it. If you find yourself writing a role comparison inline, stop — that logic belongs in policy.ts, and duplicating it there is how the two copies diverge.
If a genuinely new kind of decision is needed, extend policy.ts rather than special-casing the route. Widening RoomAction or OrganizationAction is the normal way to do that.
Also check what the response body contains. MongoDB's internal _id has leaked through before — see the fix(api): stop leaking Mongo's internal _id commit. Return domain objects, not raw documents.
4. Specification — apps/api/src/openapi.ts
Document the operation: path, method, summary, request schema, every response status you can actually return, and the required scope. The spec is served live by the running service, so an undocumented operation is invisible to every consumer that reads it.
Verify: npm run openapi writes openapi.json; apps/api/src/openapi.test.ts asserts structural properties of the document.
5. Tests — apps/api/src/app.test.ts
supertest against createApp() with a MemoryRepository. Cover, at minimum:
- The authorized path — the caller who should succeed, does.
- The denied path — a caller who should not, gets 403 (or 404 where the endpoint deliberately hides existence; there is precedent for that in the organization join-code routes, which close an existence oracle).
- Validation — a malformed body is rejected before it reaches persistence.
An endpoint whose denial is untested is not finished. The 200 path tends to be exercised by hand during development; the 403 path only ever runs in the test suite.
6. Documentation — docs/api.md
Add the route, its scope, and its authorization rule. A behavior change without a doc update is treated as incomplete in this repository, not as a follow-up.
Verify
npm run typecheck
npm test
npm run lint
npm run openapi
npm run docs:links
Report what you ran and what it said.
Checklist