Node SDK Generation: OpenAPI-Driven Clients LLMs Cannot Hallucinate

Node SDK Generation: OpenAPI-Driven Clients LLMs Cannot Hallucinate

LLMs love inventing client.getUserByEmail() that never existed. Stop asking models to remember your HTTP surface. Generate typed Node clients from OpenAPI, reject PRs that hand-edit generated files, and let agents only fill call sites against types that already compile. If it is not in the spec, TypeScript will not let it ship.

⚡ TL;DR: OpenAPI is the source of truth → openapi-typescript / orval / custom codegen → CI fails on drift → agents import the SDK only. Combine with structured codegen outputs and spec-driven Copilot Workspace patterns.

Generate once; never hand-type routes

# package.json scripts
# ✅
"gen:sdk": "openapi-typescript openapi/payments.v3.yaml -o src/gen/payments.ts && node scripts/gen-client.mjs",
"check:sdk": "git diff --exit-code -- src/gen/"

# ❌ agents invent fetch('/v3/paymnets') typos forever
// src/gen/payments.ts — generated (illustrative)
export interface paths {
  "/v3/payments/{paymentId}": {
    get: {
      parameters: { path: { paymentId: string } };
      responses: { 200: { content: { "application/json": Payment } } };
    };
  };
}

// src/sdk/paymentsClient.ts — thin wrapper over generated types
import createClient from "openapi-fetch";
import type { paths } from "../gen/payments.js";

export const payments = createClient<paths>({ baseUrl: process.env.PAYMENTS_URL! });

// ✅ app code
const { data, error } = await payments.GET("/v3/payments/{paymentId}", {
  params: { path: { paymentId } },
});

// ❌ hallucinated
// await payments.GET("/v3/payments/by-email/{email}", ...)

CI gates that make hallucination a compile error

# .github/workflows/sdk.yml
jobs:
  sdk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm gen:sdk
      - run: pnpm check:sdk          # ✅ fail if gen/ dirty
      - run: pnpm tsc -p tsconfig.json --noEmit
      - name: Ban raw fetch to payments host
        run: |
          if rg -n "PAYMENTS_URL|payments\\.internal" src --glob '!src/sdk/**' --glob '!src/gen/**'; then
            echo "Use payments SDK only"; exit 1
          fi

✅ CODEOWNERS: src/gen/ owned by platform; humans/agents do not edit.
❌ “Just update the client manually for this one endpoint.”

What agents are allowed to do

Task Allowed
Change OpenAPI + regenerate Yes (spec PR)
Edit src/gen/** by hand No
Write call sites using SDK types Yes
Add any cast to skip types No — lint ban
// eslint rule — illustrative
// ❌ no-unsafe-call / ban @ts-expect-error on sdk imports
// ✅ agent proposes openapi patch + gen:sdk in the same PR

When agents must invent behavior, constrain them with structured JSON schemas that map to OpenAPI components — not free-form TypeScript.

Closing checklist

✅ Dos
– ✅ Generate clients from OpenAPI on every spec change
– ✅ Fail CI if src/gen drifts
– ✅ Ban raw HTTP to owned services outside the SDK
– ✅ Let agents edit specs + call sites, not generated trees
– ✅ Version the OpenAPI artifact with the service

❌ Don’ts
– ❌ Don’t let models memorize endpoint lists from chat history
– ❌ Don’t commit hand-patched generated files
– ❌ Don’t weaken TypeScript to silence hallucinations
– ❌ Don’t skip contract tests for new operations
– ❌ Don’t forget property tests on client retries

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

1 Comment

Leave a Reply