Bootgs OpenAPI
Available scripts
scripts/generate-openapi.ts — walks the TS AST and writes an OpenAPI 3.0 openapi.json. Run npx tsx scripts/generate-openapi.ts --help for 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 barrel index.ts. Never share a TS type directly between frontend and backend build targets — openapi.json is the only contract boundary; the two sides may not even share a tsconfig.
- 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.json before 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-client skill 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 is RestController or Controller. 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 *Mapping aliases, or RequestMapping. The method's own string-literal argument (if any) is appended to the base path.
- For each parameter, read
@Param/@PathVariable as a path parameter, @Query/@RequestParam as a query parameter, and @Body/@RequestBody as the requestBody. The parameter's TypeScript type is resolved through the TypeChecker into a JSON Schema fragment under components.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
@RestController per 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 the parameters array — 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.json merged alongside a route change breaks every client generated from it.
Verification checklist
1---2name: bootgs-openapi3description: 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 backend and its client. Use when adding or changing a bootgs endpoint, keeping openapi.json in sync with controllers, or generating a typed client from a bootgs backend.4license: Apache-2.05---67# Bootgs OpenAPI89## Available scripts1011- **`scripts/generate-openapi.ts`** — walks the TS AST and writes an OpenAPI 3.0 `openapi.json`. Run `npx tsx scripts/generate-openapi.ts --help` for the full flag reference.1213## Why static analysis, not runtime reflection1415bootgs'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.1617## DTO-first contract workflow18191. Define or change **one request/response DTO per file** under `domain/dto/`, exported through a barrel `index.ts`. Never share a TS type directly between frontend and backend build targets — `openapi.json` is the only contract boundary; the two sides may not even share a `tsconfig`.202. 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`.213. Run the generator (see Usage below).224. Diff-review the produced `openapi.json` before 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.235. Regenerate/update the frontend client from the spec (see the `bootgs-client` skill for the transport layer the client must speak).2425## How the generator resolves a route2627For each source file in the TS program:281. Find `ClassDeclaration`s carrying a decorator whose **identifier text** is `RestController` or `Controller`. This is a textual match, not a type-checked import resolution.292. Read the first string-literal argument of that decorator as the base path.303. For each method, look for a routing decorator by the same textual matching: `Get`/`Post`/`Put`/`Delete`/`Patch`/`Head`/`Options`, their `*Mapping` aliases, or `RequestMapping`. The method's own string-literal argument (if any) is appended to the base path.314. For each parameter, read `@Param`/`@PathVariable` as a `path` parameter, `@Query`/`@RequestParam` as a `query` parameter, and `@Body`/`@RequestBody` as the `requestBody`. The parameter's TypeScript type is resolved through the `TypeChecker` into a JSON Schema fragment under `components.schemas`.325. Route placeholders (`{id}`) map directly to OpenAPI path parameter syntax — no translation needed, bootgs already uses the OpenAPI convention.3334## Usage3536```bash37npx tsx scripts/generate-openapi.ts --tsconfig ./tsconfig.appsscript.json --output ./openapi.json38```3940Wire it into `package.json`:4142```json43{ "scripts": { "openapi:generate": "tsx scripts/generate-openapi.ts --tsconfig tsconfig.appsscript.json --output openapi.json" } }44```4546And into CI as a drift check:4748```bash49npm run openapi:generate && git diff --exit-code openapi.json50```5152## Gotchas5354- **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.55- **One `@RestController` per 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.56- **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 the `parameters` array — a typo in either produces a spec where the path has a placeholder with no matching declared parameter.57- **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.58- **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.json` merged alongside a route change breaks every client generated from it.5960## Verification checklist6162- [ ] `openapi.json` reflects every route, including newly added path/query params.63- [ ] `git diff --exit-code openapi.json` passes after running the generator (nothing hand-edited it out of sync).64- [ ] Every schema referenced by an operation actually appears under `components.schemas` (no dangling `$ref`).