Skip to content

feat(api-skeleton): fastify scaffold with envelope, error mapper, rate limit, idempotency, OpenAPI#17

Merged
themightychris merged 6 commits into
mainfrom
feat/api-skeleton
May 16, 2026
Merged

feat(api-skeleton): fastify scaffold with envelope, error mapper, rate limit, idempotency, OpenAPI#17
themightychris merged 6 commits into
mainfrom
feat/api-skeleton

Conversation

@themightychris
Copy link
Copy Markdown
Member

Summary

  • Adds the Fastify application skeleton (buildApp() in apps/api/src/app.ts) with the complete plugin stack defined in the api-skeleton plan
  • Implements the response envelope (ok(), paginated(), errorResponse()) and a single setErrorHandler that maps gitsheets errors + custom API errors to the documented error codes
  • In-memory rate limiting (60 reads/min/IP, 30 writes/min/account, 10 auth/min/IP) and idempotency-key replay (24h TTL, keyed by personId+key)
  • UUIDv7 trace IDs on every request, included in error responses and pino log lines
  • OpenAPI 3.1 at /api/_openapi.json + Swagger UI at /api/_docs
  • GET /api/health is the only business endpoint; all other stubs are /_test/* routes used only by integration tests
  • .env.example with inline comments for every EnvSchema field

Test plan

  • GET /api/health returns { success: true, data: { status: 'ok' }, metadata: { timestamp } }
  • Booting with an invalid STORAGE_BACKEND throws (env validation rejects)
  • Booting with missing CFP_DATA_REPO_PATH throws
  • An intentionally-thrown ApiValidationError surfaces as 422 validation_failed with error.traceId and error.fields
  • An unknown Error surfaces as 500 internal_error with no error message leaked
  • traceId in error responses matches UUIDv7 format (/^[0-9a-f]{8}-...-7.../)
  • 61 anonymous reads from the same IP within a minute → 429 with Retry-After
  • Repeat POST with the same Idempotency-Key returns the cached response (frozen at timestamp)
  • /api/_openapi.json returns a valid OpenAPI 3.1 document (openapi: "3.1.*")
  • /api/_docs responds < 400

🤖 Generated with Claude Code

themightychris added a commit that referenced this pull request May 16, 2026
- status: done, pr: 17
- All 10 validation criteria ticked (CI green: type-check + lint + test + build pass)
- Notes: @fastify/env JSON Schema vs Zod dual-maintenance; /api/_openapi.json manual
  route (swagger-ui serves at /api/_docs/json); pluginTimeout 30s for git cold-read;
  vitest timeout 30s for worktree git ops; rate limit account-keying stubbed pending
  auth; /_test/* routes in production
- Follow-ups:
  - Issue #18: guard /_test/* stub routes in production
  - auth-jwt-substrate plan updated to absorb account-based rate-limit wiring
npm install --workspace=apps/api @fastify/env @fastify/cors @fastify/cookie @fastify/swagger @fastify/swagger-ui @fastify/rate-limit uuidv7 zod
…it, idempotency, OpenAPI

- env.ts: Zod + JSON Schema env validation; all process.env reads isolated here
- lib/response.ts: ok() / paginated() / errorResponse() envelope helpers
- lib/errors.ts: custom error classes + setErrorHandler mapper (gitsheets errors + our own)
- plugins/trace-id.ts: UUIDv7 traceId decorator on every request
- plugins/store.ts: decorates fastify.store from bootStores()
- plugins/rate-limit.ts: in-memory counters per-IP (60 reads, 30 writes, 10 auth/min)
- plugins/idempotency.ts: 24h in-memory cache keyed by (personId, Idempotency-Key)
- routes/health.ts: GET /api/health + /_test/* stubs for error/idempotency tests
- app.ts: buildApp() wires plugins in specified order; accepts overrideEnv for tests
- index.ts: updated entry point that calls buildApp()
- .env.example: all EnvSchema fields with inline comments
- /api/_openapi.json and /api/_docs served via @fastify/swagger + @fastify/swagger-ui

Plugin order: env → cors → cookie → trace-id → setErrorHandler → store → rate-limit →
idempotency → swagger → swagger-ui → routes
- tests/api-skeleton.test.ts: covers all 10 Validation criteria from the plan
  (health envelope, error mapper, 500 no-leak, traceId UUIDv7, rate limit 61→429,
  idempotency key replay, OpenAPI JSON, Swagger UI, env validation rejects)
- tests/helpers/test-full-repo.ts: createFullDataRepo() creates a gitsheets repo
  with all required sheet configs for full-app tests using openPublicStore()
- vitest.config.ts: testTimeout + hookTimeout bumped to 30s (git ops in worktrees
  run slower than the default 5s avvio/vitest timeouts)
These were installed at the workspace root but not declared in apps/api/package.json,
causing the type-check to fail in CI where each workspace's dependencies are resolved
strictly from its own package.json.
- status: done, pr: 17
- All 10 validation criteria ticked (CI green: type-check + lint + test + build pass)
- Notes: @fastify/env JSON Schema vs Zod dual-maintenance; /api/_openapi.json manual
  route (swagger-ui serves at /api/_docs/json); pluginTimeout 30s for git cold-read;
  vitest timeout 30s for worktree git ops; rate limit account-keying stubbed pending
  auth; /_test/* routes in production
- Follow-ups:
  - Issue #18: guard /_test/* stub routes in production
  - auth-jwt-substrate plan updated to absorb account-based rate-limit wiring
@themightychris themightychris merged commit a281da3 into main May 16, 2026
1 check passed
@themightychris themightychris deleted the feat/api-skeleton branch May 16, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant