feat(api): contract hardening — Problem Details, zod, pagination (#179) #190
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!190
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "179-api-contract-hardening"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Brings every API route under one shared contract so the OpenAPI spec generated by #178 has a clean substrate. Three commits:
168a2b0Problem Details — every error response now flows throughlib/api/errors.tsand matches RFC 7807 (type,title,status,detail) with Carol's existingcodefield preserved as an extension and anerrors[]per-field array when the 400 came from a zod schema. 29 of 30 routes converted;/api/healthstays bespoke (ops endpoint, not part of the v1 contract).f1c84b2zod migration —parseRegisterRequest,parseLoginRequest,parseSettingsUpdateand theValidationErrorclass are gone, replaced byzRegisterRequest,zLoginRequest,zSettingsUpdate. Granular API codes (invalid_email,invalid_password,invalid_theme) consolidate intoinvalid_bodywith the failing field's path inerrors[]. Closes the deferred follow-up called out in ADR-0012's "Out of scope" section.b596155pagination + conventions doc — addslib/api/pagination.tswith cursor-based helpers and applies it to/api/notesas the reference.docs/api-conventions.mdcodifies the full contract (status codes, error envelope, pagination, sort/filter grammar, versioning, exceptions); CLAUDE.md links to it.Decisions baked in
application/problem+json. Carol's existingcodefield preserved as the machine-readable identifier clients switch on.info.versionis authoritative. Future v2 (if it happens) uses/api/v2/*for new routes while/api/*continues to serve v1./api/notes; the other flat-list endpoints (/api/educations,/api/profile/contacts,/api/account/tokens) are domain-bounded today (a user has tens, not thousands) and remain flat arrays. Migration is staged for a follow-up — the helpers apply unchanged when those endpoints grow.?sort=-created_at,?filter[field]=valuegrammar codified in the conventions doc. No current endpoint exercises it; feature tickets pull it in when they need it./api/healthstays bespoke. Ops endpoint, not part of the v1 contract.?error=<reason>for user-facing failures; the JSON-error branches of the same routes use Problem Details.Out of scope
/api/openapi.jsonin the public-routes allowlist — #178./api/educations,/api/profile/contacts,/api/account/tokens— staged follow-up, documented inapi-conventions.md.Test plan
npm run typecheckgreennpm run testgreen — 497 tests pass (up from 495, plus 2 new pagination tests on/api/notes)git grep "NextResponse\\.json({ error" app/api/returns nothinggit grep "ValidationError\\|parseRegisterRequest\\|parseLoginRequest\\|parseSettingsUpdate" lib/dto/returns nothingcurl -i .../api/notes -H "Cookie: …"with a bad body returns a 400 Problem Details body withContent-Type: application/problem+jsoncurl .../api/notes?limit=1 -H "Cookie: …"returns{ data: [...], next_cursor, has_more }/notesafter migration — confirm list renders via the new.dataextractionCloses #179. Second ticket under epic #176 — sets the table for #178 (OpenAPI generation) and #182 (generated typed client).
Adds the cursor-pagination helper at lib/api/pagination.ts and applies it to /api/notes as the reference list endpoint. The response shape becomes { data, next_cursor, has_more }; the cursor is an opaque base64url JSON tuple of (updated_at, id). NotesRepository gains listPaginatedByUserId alongside the existing listByUserId. docs/api-conventions.md codifies the full API contract: HTTP status codes (no 403 — cross-user reads 404), the RFC 7807 envelope with Carol's code extension and the canonical code table, cursor pagination shape, the defined-not-implemented sort/filter grammar, versioning (no URL prefix; spec's info.version is authoritative), and the documented exceptions. CLAUDE.md links to it. The other flat-list endpoints (/api/educations, /api/profile/contacts, /api/account/tokens) are domain-bounded today (a user has tens, not thousands) and remain as flat arrays; the conventions doc records that migration is staged for a follow-up when those endpoints grow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>📊 Test coverage
Patch coverage: 76.1% (274/360 added lines) ⚠️ (soft target ≥ 80%)
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Changed files in this PR (source only — tests excluded):
app/api/account/tokens/[id]/route.tsapp/api/account/tokens/route.tsapp/api/auth/login/route.tsapp/api/auth/me/route.tsapp/api/auth/oauth/callback/[provider]/route.tsapp/api/auth/oauth/start/route.tsapp/api/auth/register/route.tsapp/api/contributions/[id]/route.tsapp/api/educations/[id]/route.tsapp/api/educations/route.tsapp/api/jobs/[id]/positions/route.tsapp/api/jobs/[id]/route.tsapp/api/jobs/route.tsapp/api/notes/[id]/route.tsapp/api/notes/route.tsapp/api/positions/[id]/contributions/route.tsapp/api/positions/[id]/route.tsapp/api/profile/contacts/[id]/route.tsapp/api/profile/contacts/route.tsapp/api/profile/picture/route.tsapp/api/profile/route.tsapp/api/settings/route.tsapp/api/skill-sections/[id]/move/route.tsapp/api/skill-sections/[id]/route.tsapp/api/skill-sections/route.tsapp/api/skills/[id]/move/route.tsapp/api/skills/[id]/route.tsapp/api/skills/route.tsdb/repositories/notes.tslib/api/errors.tslib/api/pagination.tslib/dto/settings.tslib/dto/user.tsSoft thresholds per ADR-0019. Coverage is informational and does not block merge.
Trivy (container image)
Threshold:
high· Total findings: 121 · At/above threshold: 16.27.0, 7.28.0, 8.5.0