| name | sdk-boundary |
| description | Enforce a strict OpenAPI → generated-SDK boundary on a TypeScript frontend. The frontend talks to the backend ONLY through a typed SDK generated from a strict OpenAPI spec; backend changes propagate as TypeScript errors. Use when reviewing frontend code that calls a backend, setting up a new frontend, or auditing an existing one for raw fetch/HTTP usage. Triggers on "openapi sdk", "regenerate the client", "sdk boundary", "remove fetch usage".
|
SDK Boundary
The frontend ↔ backend contract is one thing: a strict OpenAPI spec. The
frontend imports a typed SDK generated from that spec — never raw HTTP. Backend
changes flow to the frontend as TypeScript errors. That is the point.
Why this boundary exists
- The frontend can be scoped to its own directory. An agent working there only
needs the generated SDK and its types, not the backend codebase.
- Backend endpoint changes show up as red squiggles the next time the SDK is
regenerated. No manual sync, no drift.
- A mock server can be generated off the same spec, so designers and other
frontend devs never need to clone or boot the backend.
Rules
1. The SDK is generated, not authored
A hand-written api.service.ts, api.client.ts, or http.ts that wraps
backend endpoints is a violation. Generated files live under one configured
output directory (typically src/api/), are named *.gen.ts, and start with a
"DO NOT EDIT — autogenerated" banner.
Check: imports of fetch, HttpClient, axios, ky, got, superagent,
or any hand-rolled API service module from outside the generated directory.
Fix: replace the call site with the corresponding SDK function. If the SDK
doesn't expose the operation, the spec is wrong — fix the backend, not the
frontend.
2. Generated files are sacred
The autogeneration banner means hand edits survive exactly one regeneration and
then disappear. Never edit generated files.
Check: git log -p against the generated directory for commits that aren't
"regenerate SDK".
Fix: move customization into a wrapper module that imports the SDK, or
change the spec/config so the generator emits the desired output.
3. Operation IDs drive SDK shape
Without explicit operationIds on each endpoint, you get function names like
getUserOrdersApiV1UsersUserIdOrdersGet. Every
endpoint declares a camelCase identifier following REST verb conventions:
list*, get*, create*, update*, delete*.
Check: scan the spec for any operationId that's snake_case, repeats the
URL path, or contains an HTTP method suffix.
Fix (FastAPI shown; same idea wherever the spec is produced):
@router.get("/users", operation_id="listUsers")
@router.get("/users/{id}", operation_id="getUser")
4. Strict types — no any
tsconfig.json has strict: true and disallows any. The whole point of the
SDK is that the type system catches breaking API changes; loose types silently
swallow them.
Check: tsconfig.json for "strict": true. Lint config disallows any.
Fix: tighten the config. The SDK already produces precise types; the
project just has to honor them.
5. The spec is the contract — fix the spec, not the output
If the SDK is wrong, the spec is wrong. Editing generated code, casting away
mismatches, or writing client-side adapters to "correct" the response shape
hides drift between frontend and backend.
Check: any as, !, or // @ts-ignore adjacent to an SDK call site.
Fix: change the spec (and the backend serializer that produced it) so the
type matches reality. Then regenerate.
6. One canonical regeneration command
Exactly one way to regenerate: npm run generate:api. The script invokes the
generator directly — no npx, no shell wrappers, no per-developer paths.
Check: package.json scripts has a single generate:api entry.
Fix: collapse alternatives into the canonical script.
7. Pin the generator and document the spec source
Pin the generator's version. Document where the spec comes from — a running
backend URL or a checked-in openapi.json. If it's a live URL, the README says
the backend must be running for generate:api to work.
Check: package.json lists the generator in devDependencies with a
pinned version. The generator's config has a clear input.
Fix: pin the version. Document the source. If the spec is volatile, check
in a snapshot.
8. No raw HTTP at call sites
Components, hooks, and services import from the SDK. They never import fetch,
HttpClient, axios, or any other HTTP client directly. Auth, base URL, error
handling, and interceptors live in the SDK's client config — exactly once.
Check: grep for HTTP-client imports outside the generated directory. For
brownfield projects, add a no-restricted-imports lint rule that flags them. A
documented exception wrapper (see Exceptions) is not a violation; an
undocumented one is.
Fix: move shared HTTP concerns into the SDK client setup. Replace each
direct call with the SDK function for that operation.
Exceptions: endpoints OpenAPI can't express
Some endpoints never reach the SDK. SSE streams, WebSockets, long-poll, binary
file downloads: OpenAPI 3.x can't describe them, so the generator skips them.
Rule 5 doesn't apply. You can't fix a spec to cover what a spec can't express.
These are allowed, but kept on a short leash:
- The transport lives in one wrapper module (say,
src/api/streaming.ts), never
a raw EventSource or fetch at a call site. Auth and base URL come from the
same client config the SDK uses.
- Hand-write the transport, not the types. An SSE event's
data shape can still
be an OpenAPI component. Import the generated type and parse into it.
- Document every exception, in the wrapper or the README. "3 known non-SDK
endpoints" beats a raw fetch a reviewer can't tell from drift.
An unlisted raw fetch and a listed SSE wrapper look identical to a grep. The
list is what keeps the boundary auditable.
Setup
The rules above don't depend on a specific generator. The setup below uses
@hey-api/openapi-ts.
npm install --save-dev @hey-api/openapi-ts@latest
openapi-ts.config.ts:
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "http://localhost:8000/openapi.json",
output: "src/api",
});
package.json:
{
"scripts": {
"generate:api": "openapi-ts"
}
}
Usage:
import { listUsers, getUser } from "./api/sdk.gen";
const users = await listUsers({ query: { limit: 10 } });
const user = await getUser({ path: { user_id: 123 } });
Angular HTTP Resources
For Angular projects, the Angular plugin generates HttpResource-based
services alongside requests and types
(https://heyapi.dev/openapi-ts/plugins/angular):
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "http://localhost:8000/openapi.json",
output: "src/api",
plugins: ["@hey-api/client-fetch", "@hey-api/typescript", "@hey-api/sdk", "@hey-api/angular"],
});
Three layers come out:
- Types — request/response interfaces.
- Requests — function-based callers.
- Resources —
HttpResource wrappers for Angular's reactive resource API.
Components consume the generated resources or requests. Raw HttpClient stays
forbidden outside the generated directory — rule 8 applies unchanged.
Reporting violations
For each violation found:
- Which rule (1–8)
- Where (file:line)
- What's wrong
- Fix
Sort by impact. A hand-edited generated file (rule 2) or a raw fetch at a
call site (rule 8) is higher priority than a single missing operation ID
(rule 3).