bootgs-openapi
Generates an OpenAPI 3.0 specification from bootgs @RestController classes via static TypeScript AST analysis (not runtime reflection), and documents the DTO-first contract workflow between a bootgs b
SKILL.mdskills/bootgs-openapi/SKILL.md
GitHubBootgs OpenAPI
Available scripts
scripts/generate-openapi.ts— walks the TS AST and writes an OpenAPI 3.0openapi.json. Runnpx tsx scripts/generate-openapi.ts --helpfor the full flag reference.
Why static analysis, not runtime reflection
bootgs's own routing metadata (via reflect-metadata) only exists once a controller class is instantiated inside a live Apps Script execution — there's no server process you can query for its route table from a build machine. Generating a spec at build time means walking the TypeScript AST directly with the typescript compiler API, before any of it runs. scripts/generate-openapi.ts in this skill does exactly that: it loads your tsconfig, finds every class decorated with @RestController/@Controller, and reads its routes, parameters, and JSDoc straight off the syntax tree.
DTO-first contract workflow
- Define or change one request/response DTO per file under
domain/dto/, exported through a barrelindex.ts. Never share a TS type directly between frontend and backend build targets —openapi.jsonis the only contract boundary; the two sides may not even share atsconfig. - Add or change the controller method. Type its parameters and return value with the DTO, and give the method a JSDoc comment — the generator lifts it verbatim into the operation's
description. - Run the generator (see Usage below).
- Diff-review the produced
openapi.jsonbefore committing — an unexpected diff (a route disappearing, a schema losing a field) usually means a decorator or type the generator can't see, not a real API change. - Regenerate/update the frontend client from the spec (see the
bootgs-clientskill for the transport layer the client must speak).
How the generator resolves a route
For each source file in the TS program:
- Find
ClassDeclarations carrying a decorator whose identifier text isRestControllerorController. This is a textual match, not a type-checked import resolution. - Read the first string-literal argument of that decorator as the base path.
- For each method, look for a routing decorator by the same textual matching:
Get/Post/Put/Delete/Patch/Head/Options, their*Mappingaliases, orRequestMapping. The method's own string-literal argument (if any) is appended to the base path. - For each parameter, read
@Param/@PathVariableas apathparameter,@Query/@RequestParamas aqueryparameter, and@Body/@RequestBodyas therequestBody. The parameter's TypeScript type is resolved through theTypeCheckerinto a JSON Schema fragment undercomponents.schemas. - Route placeholders (
{id}) map directly to OpenAPI path parameter syntax — no translation needed, bootgs already uses the OpenAPI convention.
Usage
npx tsx scripts/generate-openapi.ts --tsconfig ./tsconfig.appsscript.json --output ./openapi.json
Wire it into package.json:
{ "scripts": { "openapi:generate": "tsx scripts/generate-openapi.ts --tsconfig tsconfig.appsscript.json --output openapi.json" } }
And into CI as a drift check:
npm run openapi:generate && git diff --exit-code openapi.json
Gotchas
- Identifier matching, not import resolution. A local class or a differently-sourced decorator that happens to be named
RestController/Get/etc. will be picked up as if it were bootgs's own. Don't shadow these names. - One
@RestControllerper class. The generator takes the first path string-literal it finds; a class with two controller-style decorators produces one path, silently ignoring the second. - Path param names must match exactly. The
{id}placeholder in the route string and the string passed to@Param("id", ...)are matched by exact, case-sensitive string equality to build theparametersarray — a typo in either produces a spec where the path has a placeholder with no matching declared parameter. - Complex types degrade gracefully, not silently correctly. Primitives, arrays, enums (string literal unions), and flat object/interface shapes resolve to real schemas. Generics, mapped types, and deep conditional types fall back to an open
{}schema — treat a suspiciously permissive schema in the output as a signal to simplify the DTO's type, not a generator bug to work around. - Regenerate before every PR that touches a controller. The CI drift check above is a floor, not a substitute for running it locally — a stale
openapi.jsonmerged alongside a route change breaks every client generated from it.
Verification checklist
-
openapi.jsonreflects every route, including newly added path/query params. -
git diff --exit-code openapi.jsonpasses after running the generator (nothing hand-edited it out of sync). - Every schema referenced by an operation actually appears under
components.schemas(no dangling$ref).