Appearance
API Contract Strategy
Phase: 1 Architecture Plan Status: pre-implementation contract
Contract Source
The current public contract is the legacy /api/... behavior served by NestJS. Go services may expose cleaner internal /v1/... routes, but the frontend-facing contract stays /api/... through api-gateway until an explicit frontend migration is approved.
Contract Artifacts
Required before implementing a migrated route:
- Legacy OpenAPI or route snapshot.
- Request examples from current frontend/runtime path.
- Response examples for success, validation error, auth error, tenant error, and not found where applicable.
- Go service OpenAPI for the native route.
- Gateway adapter mapping from legacy route to native route.
- Parity test cases.
Compatibility Surface
Must preserve:
/apiprefix./api/v1compatibility rewrite where clients still use it.- Bearer token behavior.
hoctapaz.accessTokencookie compatibility at gateway.X-Organization-Id.X-Request-IdandX-Correlation-Id.- legacy response envelopes.
- legacy validation and conflict semantics for high-risk flows.
- SSE event shape for import and AI classification streams.
Error Mapping
Gateway adapters must map service errors to legacy-compatible HTTP status and body shapes.
Minimum canonical internal error fields:
json
{
"code": "QUESTION_VERSION_CONFLICT",
"message": "serverVersion does not match clientVersion",
"details": {},
"requestId": "..."
}Gateway may translate this into legacy shape for /api/.... Native /v1/... can keep canonical errors as long as they are documented.
SSE Contract
SSE routes are not normal JSON endpoints. Gateway must:
- stream incrementally
- avoid buffering full response
- forward heartbeat events
- preserve event names and data payload fields
- close on client disconnect
- propagate correlation ID into worker logs where possible
Initial SSE routes:
/api/exam-import/algorithm-jobs/events/api/exam-import/algorithm-jobs/:id/events/api/questions/ai-classify/jobs/:id/events- attempt/exam event streams if used by frontend
Gateway Route States
Each route in the gateway route table has one state:
| State | Meaning |
|---|---|
legacy_proxy | Forward to NestJS unchanged. |
shadow_read | Serve legacy response, also call Go service for diff only. |
native_read | Serve Go response for read route, fallback disabled unless route-level rollback is triggered. |
native_write_shadow_validate | Write remains legacy or one side authoritative; compare resulting state. |
native_write | Go service is authoritative. |
removed | Only after frontend and ops confirm no callers. |
No route can jump directly from legacy_proxy to native_write without documented parity evidence.
When the native path differs from the legacy path, the gateway route table uses target_prefix to adapt the public /api/... path to the internal /v1/... service path. This keeps frontend edits out of scope during migration.
Route table entries may also set methods, exact, and suffix_segments for read-only cutovers inside a shared prefix. For example, GET /api/questions can be shadowed as an exact route while GET /api/questions/:id can be shadowed with suffix_segments: 1; POST /api/questions, PATCH /api/questions/:id, /api/questions/ai-classify/*, and other nested subroutes continue through the broader legacy /api/questions prefix.
Contract Test Levels
- Unit: handler and service behavior.
- Adapter: gateway request/response mapping.
- Parity: same seeded input against legacy and Go route.
- Browser/runtime: real current route where frontend behavior is critical, especially import editor saves, question types, and attempts.
Frontend Rule
Do not edit apps/web during initial backend rewrite. If a contract cannot be preserved without frontend change, stop and document the incompatibility in the risk register.