API contracts drift. A developer adds a field to a Go response struct, forgets to update the TypeScript client, and the frontend silently drops data or crashes on an unexpected shape. OpenAPI code generation eliminates this class of bug entirely by making the spec the single source of truth: change the YAML once, regenerate, and every handler signature and client type updates automatically.
The platform uses a modularized OpenAPI approach where specs are split into domain-specific files (auth, billing, search, admin) and composed into a single resolved spec at build time. On the Go side, oapi-codegen generates strict server interfaces that the compiler enforces—if your handler doesn’t match the spec, it won’t compile. On the TypeScript side, openapi-typescript generates type definitions that give the frontend full type safety against the same spec.
This two-sided generation means the API contract is enforced at compile time on both ends. No runtime validation surprises, no stale Postman collections, no “works on my machine” mismatches between what the server sends and what the client expects.
In spec-first development, the OpenAPI YAML file is written before any implementation code. This inverts the typical flow where developers write handlers first and generate docs later. The spec becomes a contract that both teams (backend and frontend) agree on before writing a single line of code, enabling parallel development against the same interface.
Rather than maintaining one monolithic OpenAPI file, the spec is split into domain modules (e.g., auth.yaml, billing.yaml, search.yaml). A build step uses $ref resolution to compose these into a single spec. This keeps each file manageable, allows domain teams to own their slice, and reduces merge conflicts.
oapi-codegen generates a Go interface where each API operation becomes a method signature. The compiler enforces that your handler implements every endpoint with exactly the right request/response types. Adding a new endpoint to the spec and regenerating immediately shows you every place that needs implementation.
openapi-typescript reads the spec and emits TypeScript type definitions for every schema, parameter, and response. These types flow into fetch wrappers or SDK clients, giving the frontend autocomplete and compile-time errors when accessing response fields that don’t exist in the spec.
The repository organizes OpenAPI specs under an api/ directory. Each domain has its own file with paths and schemas relevant to that domain. A root openapi.yaml uses $ref to pull in all domain files. At build time, a bundler (like redocly bundle) resolves all references into a single self-contained spec file.
api/
openapi.yaml # root spec, references domain files
paths/
auth.yaml # /auth/login, /auth/refresh, /auth/logout
billing.yaml # /billing/subscriptions, /billing/usage
search.yaml # /search/query, /search/index
schemas/
user.yaml # User, UserProfile, UserSettings
billing.yaml # Subscription, Invoice, UsageRecord
search.yaml # SearchQuery, SearchResult, IndexStatus
The oapi-codegen tool reads the bundled spec and generates a Go interface plus request/response types. The project uses the Chi server target, which generates a ServerInterface that maps each OpenAPI operation to a Go method. The actual handler struct must implement this interface, giving compile-time guarantees that every endpoint is handled.
// oapi-codegen configuration (cfg.yaml)
package: api
generate:
chi-server: true
models: true
embedded-spec: true
output: internal/api/gen.go
// Generated interface (simplified)
type ServerInterface interface {
// POST /auth/login
PostAuthLogin(w http.ResponseWriter, r *http.Request)
// GET /billing/subscriptions
GetBillingSubscriptions(w http.ResponseWriter, r *http.Request, params GetBillingSubscriptionsParams)
// POST /search/query
PostSearchQuery(w http.ResponseWriter, r *http.Request)
}
// Generated model
type SearchResult struct {
Hits []SearchHit `json:"hits"`
TotalCount int64 `json:"total_count"`
QueryTime float64 `json:"query_time_ms"`
}
On the frontend side, openapi-typescript generates type definitions from the same spec. These types are consumed by a thin fetch wrapper that provides type-safe API calls. The wrapper uses TypeScript generics to infer request body and response types from the operation, so calling api.post("/search/query", { body }) knows both what body must look like and what the response contains.
// Generated types (simplified)
export interface paths {
"/search/query": {
post: {
requestBody: {
content: {
"application/json": components["schemas"]["SearchQuery"];
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["SearchResult"];
};
};
};
};
};
}
export interface components {
schemas: {
SearchQuery: {
query: string;
filters?: Record<string, string[]>;
limit?: number;
offset?: number;
};
SearchResult: {
hits: components["schemas"]["SearchHit"][];
total_count: number;
query_time_ms: number;
};
};
}
Code generation runs as a go generate step and an npm script, both triggered by a Makefile target. CI validates that generated code is up to date by running generation and checking for git diffs—if a developer changes the spec without regenerating, CI fails.
# Makefile
.PHONY: api-gen
api-gen: api-bundle api-gen-go api-gen-ts
api-bundle:
npx @redocly/cli bundle api/openapi.yaml -o api/bundled.yaml
api-gen-go:
oapi-codegen --config api/cfg.yaml api/bundled.yaml
api-gen-ts:
npx openapi-typescript api/bundled.yaml -o frontend/src/api/schema.d.ts
# CI check: ensure generated files are up to date
api-check:
$(MAKE) api-gen
git diff --exit-code internal/api/gen.go frontend/src/api/schema.d.ts
Rather than using json.Marshal directly, the generated server code provides response helper functions that encode the correct status code and content type. This prevents a common bug where a handler returns a 200 with an error body, or sets the wrong Content-Type header.
// Handler implementation
func (s *Server) PostSearchQuery(w http.ResponseWriter, r *http.Request) {
var req api.SearchQuery
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
api.PostSearchQueryJSON400Response(api.Error{
Message: "invalid request body",
}).VisitPostSearchQueryResponse(w)
return
}
results, err := s.searchService.Query(r.Context(), req)
if err != nil {
api.PostSearchQueryJSON500Response(api.Error{
Message: "search failed",
}).VisitPostSearchQueryResponse(w)
return
}
api.PostSearchQueryJSON200Response(*results).VisitPostSearchQueryResponse(w)
}
The generated Chi server handler can be wrapped with middleware for authentication, rate limiting, and request logging. Because the generated code produces a standard http.Handler, existing Chi middleware works without modification.
An OpenAPI validation middleware (like kin-openapi) can validate incoming requests against the spec before they reach the handler. This catches malformed requests early and returns standardized error responses, reducing defensive validation code inside handlers.
redocly lint to detect cycles before they reach code generation.redocly lint tool can flag unused components.