Skip to content

fix: improve generated OpenAPI spec quality for SDK generation#1215

Draft
grdsdev wants to merge 7 commits into
masterfrom
claude/reverent-ramanujan-32755f
Draft

fix: improve generated OpenAPI spec quality for SDK generation#1215
grdsdev wants to merge 7 commits into
masterfrom
claude/reverent-ramanujan-32755f

Conversation

@grdsdev

@grdsdev grdsdev commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Evaluated the OpenAPI spec generated from this service's route schemas (npm run docs:export) against what SDK generators actually need, and fixed the issues found. Each fix is its own commit so the change and its rationale are easy to review independently.

  • operationId on every operation (0ebdf6e) - was missing on all 80 operations, which is what generators use to name SDK methods. Derived from the existing ROUTE_OPERATIONS constants via a shared @fastify/swagger transform instead of touching every route file.
  • Rename the wildcard path param from * to wildcard (0832ef9) - Fastify's catch-all route segment leaked straight into the spec as a parameter literally named *, which isn't a legal identifier in any target language. Doc-only rename; the live Fastify route/validation schema is untouched.
  • Register bucketSchema/objectSchema as named components (0070095) - these were already shared TS constants but got inlined by value into every route's response, so components.schemas only ever had the two globally addSchema'd entries (def-0/def-1). Switched the 5 call sites to $ref and named components after their $id instead of the default opaque def-N.
  • Document real 403/401 auth responses (bc35496) - every operation only ever documented a generic 4XX (or nothing, on the admin API). The JWT and admin API-key plugins already apply their security scheme via a single shared onRoute hook, so extended that same hook to also document the specific status each one throws.
  • Dedupe trailing-slash duplicate paths (49c2746) - /bucket, /health, and /s3/{Bucket} were each documented twice because they genuinely respond on both forms (Fastify's exposeHeadRoutes and some explicit dual registrations). Folded into one path per real endpoint via transformObject.
  • Hide the schemaless S3 protocol catch-all (8464e06) - the S3-compatible surface dispatches ~18 real commands via query-string/header matching inside an internal router, which OpenAPI can't express as distinct operations. The generic catch-all Fastify saw had no request/response schema at all - worse for SDK generation than not documenting it. Hidden, with the s3 tag description pointing at the real per-command schemas in s3/commands/*.
  • Fill in missing descriptions (e9606c3) - 50 of 62 operations had a summary but no description. Added one-liners focused on what wasn't already obvious from the summary (auth flow variant, pagination style, side effects, TUS protocol semantics).

Skipped: a servers block. This service doesn't know its own deployment prefix (Kong adds /storage/v1 in front in production; self-hosted setups vary), so a guessed base URL would be more misleading than useful.

Impact (main api.json)

before after
paths 35 29
operations 80 62
with operationId 0 60
with description 10 60
named component schemas def-0, def-1 authSchema, errorSchema, bucketSchema, objectSchema
params literally named * 43 0
/s3/** paths 4 0
responses with 403 0 40

Admin api-admin.json: paths 19→18, 401 responses 0→40.

Follow-up: which fixes are docs-only vs. real route changes

Two different mechanisms are used above, worth flagging for review:

  • Real route changes (affect the live server, not just the generated doc): the 403/401 responses (bc35496) are added via an onRoute hook that mutates the actual routeOptions.schema before Fastify compiles the route - confirmed in fastify/lib/route.js, onRoute fires on the live options object pre-registration. The bucketSchema/objectSchema refs and the added descriptions are plain edits to each route file.
  • Docs-only (@fastify/swagger transform/transformObject, never touch the live server): operationId derivation, the wildcard *wildcard rename, def-N$id ref naming, and the trailing-slash dedupe.

Looked into whether any of the docs-only ones should move to being direct route changes instead:

Fix Movable? Why / tradeoff
operationId Partially Could hardcode a string per route, but loses the single source of truth (ROUTE_OPERATIONS) and can't help Fastify's auto-synthesized HEAD routes anyway - there's no route file for those to edit; the dedup logic has to run after Fastify creates them. Not worth it.
wildcard *wildcard Yes, but risky Fastify supports named regex params (:wildcard(.*)) as an alternative to *. A real fix would mean changing the actual route pattern and every handler that reads request.params['*']. Bigger diff, touches routing semantics, needs real regression testing - not a docs change anymore.
def-N$id ref naming No Document-serialization setting, not a per-route thing - no route-level equivalent exists.
trailing-slash dedupe Yes, but risky fastify({ignoreTrailingSlash: true}) would collapse them for real, but S3's dual registrations are explicit and would likely collide with that flag. Real behavior change, needs testing.
S3 catch-all hide Yes, cleanly s3/index.ts already builds schema: { tags: ['s3'] } inline at the one place these routes are registered. Adding hide: true there directly would be simpler than the tag-sniffing check currently in the shared transform (isS3ProtocolCatchAll), and would let that check be deleted entirely.

Not applied yet - flagging for a follow-up commit if wanted.

Test plan

  • npx tsc -noEmit -p . clean after every commit
  • npm run docs:export regenerated both specs successfully after every commit (this is also how the FST_ERR_SCH_SERIALIZATION_BUILD crash from the schema-registration commit and the dropped-default-200 regression from the auth-response commit were caught before landing)
  • biome check / prettier --check clean on all touched files
  • Integration/e2e suite (npm run test:integration) not run in this session - needs Docker infra not available in the sandbox

grdsdev added 7 commits July 7, 2026 14:37
Every generated operation was missing operationId, which breaks SDK
method naming in every OpenAPI code generator. Route configs already
carry a stable, unique operation name (ROUTE_OPERATIONS) used for
observability - derive operationId from it via a swagger transform
instead of duplicating names per route.
Fastify's catch-all route segment is `*`, and @fastify/swagger mirrored
it verbatim into the spec as a path param literally named `*` - not a
legal identifier for any SDK generator. Rewrite it to `wildcard` for
docs only; the raw route/schema Fastify validates against is untouched.
/bucket, /health, and /s3/{Bucket} were each documented twice (with
and without a trailing slash) because Fastify's exposeHeadRoutes and
some routes' explicit dual registration both genuinely respond on
either form. Fold the trailing-slash variant's methods into the
slash-less path via transformObject so SDKs only see one operation
per real endpoint.

Also hardens the operationId dedup from the previous commit: it only
tried one method-based suffix, which wasn't enough once two GET routes
(and their auto-derived HEAD counterparts) legitimately share the same
config.operation - loop with a numeric suffix until unique instead.
bucketSchema and objectSchema were already shared TS constants with
their own $id, but every route embedded them by value instead of by
$ref, so the spec had no reusable models at all outside the two
globally addSchema'd ones (authSchema/errorSchema) - every response
body was duplicated inline per operation.

Register them via app.addSchema and switch the 5 call sites that
embedded them by value to $ref instead (including objectSchema's own
nested `buckets` field). Also make @fastify/swagger name components
after their $id (bucketSchema, objectSchema) instead of the default
opaque def-0/def-1 numbering.
Every operation only ever documented a generic 4XX (main API) or
nothing at all (admin API) for errors, so a generated SDK gets exactly
one exception type regardless of what actually happened. The JWT and
admin API-key plugins already have a single onRoute hook that applies
the security scheme to every route they guard - extend that same hook
to also document the specific, code-verified status each one throws
(403 for invalid JWT/role in jwt.ts, 401 for a bad admin apikey), since
that's true for every route behind it without needing to touch each
route file individually.

Guards against dropping @fastify/swagger's default "200: Default
Response" placeholder on routes that don't declare their own response
schema, which setting schema.response unconditionally would otherwise
suppress.
The S3-compatible surface dispatches ~18 real commands (PutObject,
ListObjects, CreateMultipartUpload, ...) by matching query
string/headers inside the internal s3/router.ts Router - Fastify (and
therefore @fastify/swagger) only ever sees the outer catch-all route,
with no request/response schema, since OpenAPI has no way to express
"N operations, same path and method, picked by a query parameter".

Documenting that catch-all as-is produced one method per path/verb
with an untyped body and an untyped response standing in for the
entire S3 API - worse for SDK generation than not documenting it.
Hide it instead and point the s3 tag description at the real
per-command schemas in s3/commands/*.
50 of 62 operations had a summary but no description, so generated
SDK doc comments were empty for most methods. Add one-line
descriptions across 27 route files, focused on what isn't already
obvious from the summary: auth flow differences (public/authenticated/
signed-URL variants), pagination style (cursor vs limit/offset),
side effects (CDN purge scope, x-upsert), and TUS protocol semantics
per step.
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 28900889175

Coverage decreased (-0.3%) to 78.647%

Details

  • Coverage decreased (-0.3%) from the base build.
  • Patch coverage: 53 uncovered changes across 1 file (10 of 63 lines covered, 15.87%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
src/http/routes/openapi-transform.ts 56 3 5.36%
Total (5 files) 63 10 15.87%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 12446
Covered Lines: 10234
Line Coverage: 82.23%
Relevant Branches: 7200
Covered Branches: 5217
Branch Coverage: 72.46%
Branches in Coverage %: Yes
Coverage Strength: 419.21 hits per line

💛 - Coveralls

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.

2 participants