OpenAPI Code Generation & SDK Automation

Web & API Engineering intermediate Building with AI ~18 min read

Overview

When a project exposes a REST API, every consumer—web dashboard, CLI tool, mobile app, integration test suite—needs to know the exact shape of requests and responses. Manually writing client code for each consumer is tedious, error-prone, and falls out of sync the moment someone adds a field to the API without updating every client. OpenAPI code generation eliminates this problem by treating the OpenAPI 3.0 specification as the single source of truth and auto-generating type-safe client SDKs, server stubs, request validation, and documentation from it.

A polyglot project can use this approach across multiple languages. The Go backend uses oapi-codegen to generate server interfaces and request/response types from the OpenAPI spec, ensuring that the implementation stays in sync with the documented contract. The TypeScript frontend uses openapi-typescript to generate TypeScript types and openapi-fetch to create a fully-typed HTTP client that provides autocomplete for every endpoint, path parameter, query parameter, and response shape. Changes to the API spec automatically propagate to all consumers at compile time.

This document covers the OpenAPI specification format, code generation toolchains for Go and TypeScript, patterns for keeping generated code in sync with handwritten business logic, and strategies for versioning and backward compatibility.

Core Concepts

OpenAPI 3.0 Specification

An OpenAPI spec is a YAML or JSON document that describes every endpoint, parameter, request body, and response schema of a REST API. It uses JSON Schema for type definitions and supports composition via $ref, allOf, oneOf, and anyOf. The spec serves as both human-readable documentation and machine-readable contract: tools can generate code, validate requests, produce documentation sites, and run contract tests from the same file.

Server-Side Code Generation (Go)

oapi-codegen reads an OpenAPI spec and emits Go interfaces, request/response structs, and routing code for popular frameworks (Chi, Echo, Gin, net/http). The generated server interface has one method per operation, and each method receives a typed parameters struct and returns a typed response. The developer implements the interface with business logic; the generated routing layer handles parameter parsing, validation, and response serialization. If the spec changes and the implementation no longer satisfies the interface, the Go compiler catches the mismatch.

Client-Side Code Generation (TypeScript)

openapi-typescript converts the OpenAPI spec into TypeScript type definitions—one type per schema, plus path/operation types that map endpoint URLs to their parameters and responses. openapi-fetch then uses these types to provide a fetch wrapper where client.GET("/collections/{id}", { params: { path: { id } } }) is fully typed: the path, query params, request body, and response body all have compile-time type checking and IDE autocomplete, with zero runtime overhead beyond a standard fetch call.

Schema Composition with $ref

Large APIs define shared schemas in components/schemas and reference them with $ref: "#/components/schemas/Collection". This enables reuse (the same Error schema across all error responses), modularity (split the spec into multiple files), and cleaner generated code (shared types instead of duplicated inline definitions). Code generators resolve $ref pointers and emit named types, producing a clean type hierarchy that mirrors the API’s domain model.

Contract-First vs Code-First

In contract-first development, the OpenAPI spec is written before the implementation, and code is generated from it. In code-first development, the implementation comes first, and the spec is generated from code annotations. The contract-first approach treats the spec as the authoritative artifact, and both server and client code are derived from it. This ensures that the API contract is a deliberate design decision rather than an accident of implementation.

How It Works

Writing the OpenAPI Spec

The API spec defines endpoints, parameters, request/response schemas, and authentication requirements. Here is a simplified example of a collection management API spec:

openapi: "3.0.3"
info:
  title: ExampleDB API
  version: "1.0.0"
paths:
  /collections/{collection_name}:
    get:
      operationId: getCollection
      summary: Get collection details
      parameters:
        - name: collection_name
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Collection details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CollectionInfo"
        "404":
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    put:
      operationId: createCollection
      summary: Create or update a collection
      parameters:
        - name: collection_name
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCollectionRequest"
      responses:
        "200":
          description: Collection created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CollectionInfo"
components:
  schemas:
    CollectionInfo:
      type: object
      required: [name, vectors_count, status]
      properties:
        name:
          type: string
        vectors_count:
          type: integer
          format: int64
        status:
          type: string
          enum: [green, yellow, red]
    CreateCollectionRequest:
      type: object
      required: [vectors]
      properties:
        vectors:
          $ref: "#/components/schemas/VectorsConfig"
    VectorsConfig:
      type: object
      required: [size, distance]
      properties:
        size:
          type: integer
        distance:
          type: string
          enum: [Cosine, Euclid, Dot]
    Error:
      type: object
      required: [status, message]
      properties:
        status:
          type: string
        message:
          type: string

Generating Go Server Code

The oapi-codegen tool generates Go types and a server interface from the spec. The generated interface enforces that the implementation handles every endpoint with the correct parameter and response types:

// Generated by oapi-codegen from the OpenAPI spec
//go:generate oapi-codegen --config=oapi-config.yaml api.yaml

type CollectionInfo struct {
    Name         string `json:"name"`
    VectorsCount int64  `json:"vectors_count"`
    Status       string `json:"status"`
}

type CreateCollectionRequest struct {
    Vectors VectorsConfig `json:"vectors"`
}

type VectorsConfig struct {
    Size     int    `json:"size"`
    Distance string `json:"distance"`
}

type ServerInterface interface {
    GetCollection(w http.ResponseWriter, r *http.Request, collectionName string)
    CreateCollection(w http.ResponseWriter, r *http.Request, collectionName string)
}

// Implementation satisfies the generated interface
type APIHandler struct {
    store *CollectionStore
}

func (h *APIHandler) GetCollection(w http.ResponseWriter, r *http.Request, collectionName string) {
    coll, err := h.store.Get(r.Context(), collectionName)
    if err != nil {
        writeError(w, http.StatusNotFound, "collection not found")
        return
    }
    writeJSON(w, http.StatusOK, CollectionInfo{
        Name:         coll.Name,
        VectorsCount: coll.VectorsCount,
        Status:       coll.Status.String(),
    })
}

func (h *APIHandler) CreateCollection(w http.ResponseWriter, r *http.Request, collectionName string) {
    var req CreateCollectionRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid request body")
        return
    }
    coll, err := h.store.Create(r.Context(), collectionName, req.Vectors)
    if err != nil {
        writeError(w, http.StatusInternalServerError, err.Error())
        return
    }
    writeJSON(w, http.StatusOK, coll.ToInfo())
}

Generating TypeScript Client Code

On the frontend, openapi-typescript generates types, and openapi-fetch creates a typed client. The result is a fully type-safe API client with zero manual type definitions:

// Generated types from openapi-typescript
import type { paths } from "./generated/api";
import createClient from "openapi-fetch";

const client = createClient<paths>({
  baseUrl: "https://api.exampledb.dev",
  headers: { Authorization: `Bearer ${token}` },
});

// Fully typed: path params, response body, error shape
const { data, error } = await client.GET("/collections/{collection_name}", {
  params: { path: { collection_name: "embeddings" } },
});

if (error) {
  // error is typed as { status: string; message: string }
  console.error(error.message);
} else {
  // data is typed as CollectionInfo
  console.log(data.name, data.vectors_count, data.status);
}

// Create a collection - request body is type-checked
const { data: newColl } = await client.PUT("/collections/{collection_name}", {
  params: { path: { collection_name: "new-index" } },
  body: {
    vectors: { size: 768, distance: "Cosine" },
  },
});

Build Integration

Code generation runs as a build step, typically via go generate in Go and an npm script in TypeScript. The CI pipeline regenerates code from the spec and fails if the output differs from what is checked in, ensuring that generated code is always up to date:

# Makefile target for Go code generation
.PHONY: generate
generate:
	oapi-codegen --config=oapi-config.yaml api/openapi.yaml > internal/api/generated.go

# package.json script for TypeScript
# "scripts": {
#   "generate:api": "openapi-typescript api/openapi.yaml -o src/generated/api.ts",
#   "ci:check-generated": "npm run generate:api && git diff --exit-code src/generated/"
# }

Practical Applications

In a polyglot service, OpenAPI code generation plays a central role in maintaining consistency across the codebase:

Implementation Patterns

Spec-Driven Development Workflow

The recommended workflow is: (1) edit the OpenAPI spec, (2) run code generation, (3) fix compile errors in the implementation, (4) write tests, (5) commit. This ensures the spec drives the implementation rather than the other way around. PR reviews start by reviewing the spec diff, which is more readable than code changes.

Handling Breaking Changes

When a spec change removes a field or changes a type, all generated clients break at compile time. This is a feature: it forces the team to coordinate breaking changes. For gradual rollouts, use API versioning (path-based like /v2/collections or header-based) and generate separate client types for each version.

Custom Extensions

OpenAPI supports vendor extensions (x- prefixed fields) for tool-specific configuration. For example, a Go project can use x-go-type to map OpenAPI types to existing Go types and x-oapi-codegen-extra-tags to add struct tags like validate:"required" to generated fields.

Common Pitfalls

Further Reading