fix: improve generated OpenAPI spec quality for SDK generation#1215
Draft
grdsdev wants to merge 7 commits into
Draft
fix: improve generated OpenAPI spec quality for SDK generation#1215grdsdev wants to merge 7 commits into
grdsdev wants to merge 7 commits into
Conversation
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.
Coverage Report for CI Build 28900889175Coverage decreased (-0.3%) to 78.647%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.operationIdon every operation (0ebdf6e) - was missing on all 80 operations, which is what generators use to name SDK methods. Derived from the existingROUTE_OPERATIONSconstants via a shared@fastify/swaggertransform instead of touching every route file.*towildcard(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.bucketSchema/objectSchemaas named components (0070095) - these were already shared TS constants but got inlined by value into every route's response, socomponents.schemasonly ever had the two globallyaddSchema'd entries (def-0/def-1). Switched the 5 call sites to$refand named components after their$idinstead of the default opaquedef-N.bc35496) - every operation only ever documented a generic4XX(or nothing, on the admin API). The JWT and admin API-key plugins already apply their security scheme via a single sharedonRoutehook, so extended that same hook to also document the specific status each one throws.49c2746) -/bucket,/health, and/s3/{Bucket}were each documented twice because they genuinely respond on both forms (Fastify'sexposeHeadRoutesand some explicit dual registrations). Folded into one path per real endpoint viatransformObject.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 thes3tag description pointing at the real per-command schemas ins3/commands/*.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
serversblock. This service doesn't know its own deployment prefix (Kong adds/storage/v1in front in production; self-hosted setups vary), so a guessed base URL would be more misleading than useful.Impact (main
api.json)operationIddescriptiondef-0,def-1authSchema,errorSchema,bucketSchema,objectSchema*/s3/**paths403Admin
api-admin.json: paths 19→18,401responses 0→40.Follow-up: which fixes are docs-only vs. real route changes
Two different mechanisms are used above, worth flagging for review:
bc35496) are added via anonRoutehook that mutates the actualrouteOptions.schemabefore Fastify compiles the route - confirmed infastify/lib/route.js,onRoutefires on the live options object pre-registration. ThebucketSchema/objectSchemarefs and the added descriptions are plain edits to each route file.@fastify/swaggertransform/transformObject, never touch the live server):operationIdderivation, the wildcard*→wildcardrename,def-N→$idref naming, and the trailing-slash dedupe.Looked into whether any of the docs-only ones should move to being direct route changes instead:
operationIdROUTE_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(.*)) as an alternative to*. A real fix would mean changing the actual route pattern and every handler that readsrequest.params['*']. Bigger diff, touches routing semantics, needs real regression testing - not a docs change anymore.def-N→$idref namingfastify({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/index.tsalready buildsschema: { tags: ['s3'] }inline at the one place these routes are registered. Addinghide: truethere 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 commitnpm run docs:exportregenerated both specs successfully after every commit (this is also how theFST_ERR_SCH_SERIALIZATION_BUILDcrash from the schema-registration commit and the dropped-default-200 regression from the auth-response commit were caught before landing)biome check/prettier --checkclean on all touched filesnpm run test:integration) not run in this session - needs Docker infra not available in the sandbox