feat: typed REST communication layer between web and api (#11)#33
Conversation
Decide on the web/api communication method (issue #11): REST over HTTP with an OpenAPI document as the contract source of truth and openapi-typescript-generated types consumed via Nuxt $fetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refine the design after evaluating a full generated client (SDK) against a lighter types-only option, guided by the Nuxt 4 docs: keep REST + OpenAPI, generate types + Zod via hey-api (SDK plugin off), and consume Nuxt-natively through a $api $fetch instance with Zod validation in transform. SDK / TanStack Query / transformers explicitly deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (10)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthroughImplements OpenAPI-driven typed REST: API generates committed ChangesTyped REST API Contract Implementation
Sequence DiagramssequenceDiagram
participant GenerateScript as generate:openapi script
participant GenerateEntrypoint as generate-open-api.ts
participant ApiModule as ApiModule
participant SwaggerModule as SwaggerModule
participant FileSystem as apps/api/openapi.json
GenerateScript->>GenerateEntrypoint: pnpm build + node dist/src/entrypoints/generate-open-api.js
GenerateEntrypoint->>ApiModule: NestFactory.create(ApiModule)
GenerateEntrypoint->>SwaggerModule: createOpenApiDocument(app)
SwaggerModule->>GenerateEntrypoint: OpenAPI document
GenerateEntrypoint->>FileSystem: write formatted openapi.json
sequenceDiagram
participant Component as Nuxt Component
participant useApiStatus as useApiStatus Composable
participant ApiPlugin as $api Plugin (useAsyncData)
participant ApiServer as API Server
participant ZodSchema as zGetApiInfoResponse Schema
Component->>useApiStatus: call useApiStatus()
useApiStatus->>ApiPlugin: useAsyncData fetch /v1/status via $api
ApiPlugin->>ApiServer: GET /api/v1/status (baseURL from runtimeConfig)
ApiServer->>ApiPlugin: raw JSON response
ApiPlugin->>ZodSchema: zGetApiInfoResponse.parse(response) in transform
ZodSchema->>ApiPlugin: validated GetApiInfoResponse
ApiPlugin->>Component: typed data
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
apps/api/src/entrypoints/generate-open-api.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. apps/api/src/entrypoints/tests/generate-open-api.e2e.test.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. apps/api/src/modules/status/use-cases/get-api-info/tests/get-api-info.e2e.test.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/api/src/entrypoints/tests/generate-open-api.e2e.test.ts (1)
36-43: ⚡ Quick winAssert required fields, not only property names.
This currently passes even if one of the contract fields becomes optional in OpenAPI. Add an assertion on
schema.requiredto lock the interface shape.Proposed test hardening
const schema = document.components?.schemas?.['GetApiInfoResponse'] as Record<string, any> + assert.deepEqual((schema.required ?? []).sort(), [ + 'commit', + 'name', + 'nodeEnv', + 'uptimeSeconds', + 'version', + ]) assert.deepEqual(Object.keys(schema.properties).sort(), [ 'commit', 'name', 'nodeEnv', 'uptimeSeconds', 'version', ])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/entrypoints/tests/generate-open-api.e2e.test.ts` around lines 36 - 43, The test currently only asserts property names on the OpenAPI schema for 'GetApiInfoResponse' (variable schema) but doesn't assert which fields are required; update the test to also assert schema.required contains the exact required field names (e.g., ['commit','name','nodeEnv','uptimeSeconds','version']) so the contract fails if any field becomes optional. Locate the assertion block around the schema variable in generate-open-api.e2e.test.ts and add a check that schema.required (sorted) strictly equals the expected required fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-26-web-api-communication.md`:
- Around line 486-489: The markdown code fence containing the two-path snippet
("apps/web/app/api/" and "apps/api/openapi.json") is unlabeled and triggers
MD040; update that fence to specify a language label (use "text") so the block
becomes a fenced code block with ```text at the start and ``` at the end,
preserving the existing lines inside exactly as shown; locate the unlabeled
fence around the snippet and add the language token to fix the markdownlint
warning.
In `@docs/superpowers/specs/2026-05-26-web-api-communication-design.md`:
- Line 5: Update the design status line "Status: Approved (design);
implementation pending" to indicate implementation is complete by replacing it
with a clear implemented state (e.g., "Status: Implemented" or "Status:
Implemented — [YYYY-MM-DD]") and optionally add an explicit implementation date
or state transition note so the document reflects the delivered implementation;
look for the exact phrase "Status: Approved (design); implementation pending" in
the document and modify it accordingly.
- Around line 75-89: The fenced architecture block that begins with the line
"NestJS controllers + `@nestjs/swagger` decorators" is missing a language tag;
update the opening fence from ``` to ```text (and keep the closing fence as ```)
so the block is marked as plain text to satisfy markdownlint MD040.
---
Nitpick comments:
In `@apps/api/src/entrypoints/tests/generate-open-api.e2e.test.ts`:
- Around line 36-43: The test currently only asserts property names on the
OpenAPI schema for 'GetApiInfoResponse' (variable schema) but doesn't assert
which fields are required; update the test to also assert schema.required
contains the exact required field names (e.g.,
['commit','name','nodeEnv','uptimeSeconds','version']) so the contract fails if
any field becomes optional. Locate the assertion block around the schema
variable in generate-open-api.e2e.test.ts and add a check that schema.required
(sorted) strictly equals the expected required fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f78aad67-9064-4d43-9c90-e76193f50b3f
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
.github/workflows/ci.yml.prettierignoreapps/api/openapi.jsonapps/api/package.jsonapps/api/src/entrypoints/generate-open-api.tsapps/api/src/entrypoints/tests/generate-open-api.e2e.test.tsapps/api/src/modules/status/use-cases/get-api-info/get-api-info.controller.tsapps/api/src/modules/status/use-cases/get-api-info/get-api-info.response.tsapps/api/src/modules/swagger/build-api-documentation.tsapps/web/app/api/index.tsapps/web/app/api/types.gen.tsapps/web/app/api/zod.gen.tsapps/web/app/composables/__tests__/useApiStatus.spec.tsapps/web/app/composables/useApiStatus.tsapps/web/app/plugins/api.tsapps/web/eslint.config.mjsapps/web/nuxt.config.tsapps/web/openapi-ts.config.tsapps/web/package.jsondocs/superpowers/plans/2026-05-26-web-api-communication.mddocs/superpowers/specs/2026-05-26-web-api-communication-design.mdpnpm-workspace.yaml
Embed the API version in the operationId so hey-api's generated operation-response type (GetApiInfoV1Response) no longer collides with the schema type (GetApiInfoResponse), dropping the GetApiInfoResponse2 alias. Matches the version-suffixed operationId convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root: cross-cutting REST+OpenAPI overview and the CI drift-check rule. api: OpenAPI generation + version-suffixed operationId convention. web: generated types/Zod client, $api+transform call pattern, and which generated type the FE binds to (the schema body, not operation wrappers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads → useAsyncData + $api wrapped in a per-endpoint composable; mutations/handlers → imperative $api; useFetch/useAPI are read-only setup-time variants and an anti-pattern in event handlers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/web/.claude/CLAUDE.md (1)
63-69: ⚡ Quick winClarify the URL path mismatch between generated types and actual usage.
The guidance states call sites use version-relative paths like
/v1/status(line 61), and the example at line 86 shows$api('/v1/status'). However, the generated types (context snippet 2) defineurl: '/api/v1/status'with the/apiprefix included. This mismatch could confuse developers who inspect the generated types and expect to use the literal URL string from the schema.Add a note explaining that while the OpenAPI document (and generated types) includes the full
/api/v1/...path, actual$apicalls should omit/apibecauseapiBasealready includes it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/.claude/CLAUDE.md` around lines 63 - 69, The generated types/schemas (e.g. GetApiInfoResponse, zGetApiInfoResponse from openapi.json) include the full path prefix "/api/v1/...", but runtime calls use the $api fetch instance which already applies apiBase (so call sites should use "/v1/..." not "/api/v1/..."); update the documentation to explicitly state this mismatch and instruct developers to omit the "/api" prefix when calling $api (and to continue validating responses with the generated Zod schemas in transform), and mention $api and apiBase by name so readers can find the fetch plugin and the generated schemas.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/.claude/CLAUDE.md`:
- Line 100: The example in the useFetch invocation incorrectly references a
non-existent local identifier apiBase; update the example to call
useRuntimeConfig().public.apiBase instead so the baseURL is read from runtime
config. Locate the useFetch example (symbol: useFetch<T>('/v1/...', { baseURL:
apiBase, transform })) and replace the baseURL value with
useRuntimeConfig().public.apiBase, ensuring the example imports or assumes
useRuntimeConfig is available in component scope.
- Line 102: The documentation incorrectly states that useAPI is a createUseFetch
instance bound to $api; locate the section referencing useAPI/createUseFetch in
CLAUDE.md and update it to reflect the actual implementation: note that
apps/web/app/plugins/api.ts exposes $api via $fetch.create and that there is no
useAPI or createUseFetch in the repo today — either remove the createUseFetch
claim, mark useAPI as a future/optional idea (not implemented), or reword to
explain that $api exists but no useAPI helper is provided; reference the symbols
useAPI, createUseFetch, $api, and apps/web/app/plugins/api.ts when making the
edit.
---
Nitpick comments:
In `@apps/web/.claude/CLAUDE.md`:
- Around line 63-69: The generated types/schemas (e.g. GetApiInfoResponse,
zGetApiInfoResponse from openapi.json) include the full path prefix
"/api/v1/...", but runtime calls use the $api fetch instance which already
applies apiBase (so call sites should use "/v1/..." not "/api/v1/..."); update
the documentation to explicitly state this mismatch and instruct developers to
omit the "/api" prefix when calling $api (and to continue validating responses
with the generated Zod schemas in transform), and mention $api and apiBase by
name so readers can find the fetch plugin and the generated schemas.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e8f1cd97-f2f9-4e7b-8efb-4fed71fcb904
📒 Files selected for processing (1)
apps/web/.claude/CLAUDE.md
* docs: back-fill AgDR-0001 for web↔api communication decision - Records the REST + OpenAPI + generated types/Zod decision in the canonical docs/agdr/ location (first AgDR for marsa) - Condenses the design spec from PR #33 into the AgDR template: context, options considered (tRPC/GraphQL/types-only/SDK), decision, consequences, artifacts - Makes the decision discoverable via the portfolio /agdr search Refs #11 * fix: formatting
Summary
Resolves #11 — decides and implements the communication method between
apps/webandapps/api.apps/api/openapi.jsonfrom a dedicated@nestjs/swaggergeneration entrypoint (mirrors the real server's/apiprefix + URI versioning).@hey-api/openapi-tsgenerates TypeScript types + Zod schemas (SDK plugin off); calls go through a$fetch.create$apiplugin (base URL from runtime config) with Zod validation at the response boundary in auseAsyncDatatransform./api/v1/statusendpoint.tRPC and GraphQL were considered and rejected (tRPC closes the public-API door / is non-idiomatic in NestJS; GraphQL is over-engineered for this surface). A full generated SDK + TanStack Query / neverthrow / transformers were explicitly deferred — all reversible since the OpenAPI doc remains the source of truth.
Design spec and implementation plan:
docs/superpowers/specs/2026-05-26-web-api-communication-design.md,docs/superpowers/plans/2026-05-26-web-api-communication.md.Test Plan
pnpm --filter api test) — incl. OpenAPI document shape testpnpm --filter web test) — incl. Zod boundary validation (accepts valid, rejects contract-violating payload)format:check,lint,typecheck(api + web),build:weball pass🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Chores
Documentation