Skip to content

feat(api): upgrade libnest to v9 and document request bodies from their Zod schemas - #1596

Merged
joshunrau merged 7 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:refactor-docs-and-dto
Oct 1, 2026
Merged

joshunrau merged 7 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:refactor-docs-and-dto

Conversation

@joshunrau

Copy link
Copy Markdown
Collaborator

Summary

Upgrades apps/api to @douglasneuroinformatics/libnest v9, which replaces @nestjs/swagger with a Zod-native OpenAPI generator, and removes @nestjs/swagger from the app.

libnest 9 documents a request body from the Zod schema its @Body() parameter is typed with. Anything else, including the old @ValidationSchema DTO classes, is documented as an empty schema. So:

  • DTOs replaced: all 18 DTO classes are gone. Each body is typed directly as its schema (@Body() data: $CreateGroupData), which drives both validation and the docs.
  • Types renamed: to make that possible, the 19 request-body types in packages/schemas now share their schema's $-prefixed name (CreateGroupData → $CreateGroupData). Every consumer in api, web, demo and testing is updated. This is the documented "type and value at the same call site" variant from packages/schemas/AGENTS.md.
  • New schema: PATCH /v1/assignments/:id only accepts status: 'CANCELED'. That narrowing moves from a DTO into $CancelAssignmentData.
  • Decorators: ApiOperation now comes from libnest, and @ApiTags/@ApiProperty are removed. Operations are tagged by controller name, so the hand-written tags list in main.ts is gone.
  • Paths: paths in /spec.json now carry the /v1 prefix the routes are served at.

Verification

  • Spec: OpenAPI 3.1, 63 operations; all 26 request bodies documented, none empty; no generator warnings at startup.
  • Browser: checked the Redoc page at the API root with agent-browser. Request bodies render with field types, required markers, enums and payload samples.
  • Tests: pnpm lint and pnpm test pass (1553 tests).
  • e2e: pnpm test:e2e passed 253/254. The one failure, subject-detail.spec.ts "should plot a selected measure on the graph tab", is on a code path this PR doesn't touch, and it passed 6/6 when re-run alone (3× chromium, 3× firefox).

New tests:

  • apps/api/test/suites/01-boot.suite.ts checks that docs paths are versioned, that no request body is documented as an empty schema, and that POST /v1/groups has its fields.
  • testing/src/specs/api-docs.spec.ts renders the docs page in Chromium and checks the create-group body's fields.

Known issue (pre-existing, not addressed here)

Opened through the web origin's /api/ proxy (and Caddy's in production), the docs page shows "Document must be JSON object, got string". libnest's HTML uses an absolute spec-url="/spec.json", which resolves to the SPA rather than the API. libnest 8.4.1 built the URL the same way, so this is not a regression; the fix belongs in libnest (a relative spec URL). The e2e spec reaches the page on the API's own origin for that reason.

Not carried over

The free-text @ApiProperty descriptions and examples are not in the generated docs. They could be restored with .describe() / .meta() on the schemas in a follow-up.

🤖 Generated with Claude Code

joshunrau and others added 7 commits October 1, 2026 12:13
Rename the inferred type of every schema an API route parses as a
request body to the schema's own $-prefixed name (CreateGroupData ->
$CreateGroupData, and so on), and update every consumer.

One identifier that is both a type and a value is what lets a controller
type a @Body() parameter as the schema itself, which libnest v9 needs to
validate and document it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH /v1/assignments/:id only lets a client cancel an assignment; the
narrowing to status 'CANCELED' lived in an apps/api DTO. Move it here as
$CancelAssignmentData so the controller can type its body as a schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libnest 9 replaces @nestjs/swagger with a Zod-native OpenAPI generator,
so the app no longer depends on it directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libnest 9 generates the OpenAPI document from the Zod schemas that
controller parameters are typed with, and documents any other type as an
empty schema. Replace all 18 @ValidationSchema DTO classes with bodies
typed as their schemas, so every request body is both validated and
documented from one source.

Import ApiOperation from libnest and drop @apitags: operations are now
tagged by controller name, so the hand-written tag list in main.ts goes
too. Paths in the document now carry the /v1 prefix the routes are
served at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenAPI document is now built after versioning, so its paths carry
the /v1 prefix. Fail on any request body documented as an empty schema,
which is what a body typed as a class rather than a Zod schema produces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load the Redoc page the API serves and check that a request body's
fields render from its Zod schema. The page is reached on the API's own
origin through a new apiURL, because its spec URL is absolute and the
web origin's /api proxy does not route it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the DTO-class pattern in the api AGENTS.md, the add-endpoint
playbook and the libnest notes with bodies typed as their Zod schemas,
and stop crediting the NestJS Swagger module in the API reference page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joshunrau
joshunrau merged commit 5201939 into DouglasNeuroInformatics:main Oct 1, 2026
2 checks passed
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