Concept
The beginner framing: once an API has real consumers, other teams, external developers, someone needs a precise, unambiguous description of every endpoint, its inputs, its outputs, and its error shapes, that isn't just "read the source code" or a hand-written doc that inevitably goes stale.
OpenAPI vs. Swagger, a real naming distinction worth getting right
"Swagger" (original, pre-2016): both the SPECIFICATION and the TOOLING
OpenAPI Specification (current): the SPEC itself, now under the
Linux Foundation / OpenAPI Initiative
"Swagger" (current usage): the TOOLING built around the spec, Swagger UI, Swagger Editor, Swagger CodegenThe specification was donated to and renamed under the OpenAPI Initiative in 2016, "Swagger" and "OpenAPI" are NOT interchangeable synonyms today, even though casual usage often treats them that way. The current, correct distinction: OpenAPI refers to the specification format itself (the YAML/JSON schema describing an API); Swagger now specifically refers to the SmartBear-maintained tooling ecosystem (Swagger UI for interactive docs, Swagger Editor for authoring) that CONSUMES an OpenAPI spec, you write an OpenAPI document, and Swagger UI is one of several tools that can render it into browsable documentation.
What an OpenAPI document actually captures
paths:
/users/{id}:
get:
summary: Get a user by ID
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
'200':
description: User found
content:
application/json:
schema:
type: object
properties:
id: { type: integer }
name
A real OpenAPI document is machine-readable YAML/JSON describing every path, method, parameter, request/response body shape (down to individual field types), and possible status codes, including error responses, which can reference the same RFC 9457 problem-details shape covered in API Design & Versioning. Because it's structured and machine-readable (not prose), tooling can DO things with it beyond just displaying it: generate interactive "try it" documentation (Swagger UI), generate client SDKs in many languages, generate server-side request validation, and run contract tests that verify the actual API behavior matches what the spec claims.
The core risk: documentation that isn't generated FROM the code will drift
The single most important practical lesson: an OpenAPI document hand-written and maintained SEPARATELY from the actual route implementations WILL drift out of sync over time, a developer changes a response field, forgets to update the spec (or doesn't even know it exists), and now the "documentation" actively lies about what the API does, which is arguably worse than no documentation at all, since it actively misleads consumers who trust it. The reliable pattern is generating the OpenAPI spec FROM the actual code (via annotations/decorators the framework reads, or via a schema library like zod that ALSO drives runtime validation), making the spec a genuine reflection of reality rather than a parallel, driftable artifact, the same underlying principle tRPC takes to its logical extreme by eliminating the separate spec entirely.
Try It
Predict the outcome before checking the solution.
A team's OpenAPI spec, hand-maintained, still shows:
GET /users/{id} → 200 { id, name, email }
The ACTUAL route handler was changed 3 months ago to:
GET /users/{id} → 200 { id, name, emailAddress } // renamed, spec never updatedA new developer generates a TypeScript client SDK from this OpenAPI spec and writes code using user.email. What happens?
Solution
The generated client code compiles fine (TypeScript trusts the spec, which claims email exists) but FAILS AT RUNTIME, user.email is undefined, since the real API actually returns emailAddress. This is exactly the failure mode hand-maintained, code-independent specs are prone to: the generated SDK provides a false sense of safety, since it looks and behaves like a type-checked client, but its types are only as accurate as the spec document, which in this case is simply wrong. This is the sharpest practical contrast with tRPC's approach: tRPC's "spec" IS the literal server code, so this exact failure mode is structurally impossible there, a renamed field breaks the TypeScript BUILD, not a runtime call. OpenAPI's cross-language reach is a real, genuine advantage tRPC doesn't have, but it comes with this specific, real drift risk that a team must actively guard against (typically by generating the spec FROM code, not hand-maintaining it separately).
Implement It Yourself
Build a minimal OpenAPI-path-object generator FROM a zod schema, the actual mechanism that keeps a spec honest by deriving it from code rather than hand-writing it:
const { z } = require("zod");
// A schema that ALSO drives runtime validation elsewhere in the app, reused, not duplicated:
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
});
function zodToOpenApiSchema(zodSchema) {
const shape = zodSchema.shape;
const properties = {};
for (const [key, fieldSchema] of Object.entries(shape)) {
properties[key] = { type: fieldSchema._def.typeName ===
The mechanism: instead of a human writing the OpenAPI YAML by hand (a separate, driftable artifact), the spec is DERIVED from the same schema object that's already doing real work elsewhere (runtime validation), a rename to UserSchema is now a single edit that propagates everywhere it's used, structurally preventing the exact drift scenario from Try It, without going as far as eliminating the separate spec format entirely (which is tRPC's more radical approach).
Under the Hood
This topic's central drift concern is the same practical problem tRPC & End-to-End Type Safety solves via a completely different, more radical strategy, eliminating the separate spec artifact entirely, rather than trying to keep a separate one honest through code generation. OpenAPI's genuine advantage over tRPC is real and worth naming directly: it works across ANY language (an OpenAPI-described API can generate clients in Python, Go, Java, Swift, tRPC fundamentally cannot), which matters enormously for public, third-party-consumed APIs, the same audience REST Design & Best Practices frames as REST's strongest fit over gRPC.
Common Mistakes
1. Hand-maintaining an OpenAPI spec entirely separately from the route implementations
# openapi.yaml, last updated 3 months ago
# actual route code, changed weeklyAs shown in Try It, a spec that isn't mechanically derived from (or driving) the actual implementation WILL drift, and a wrong spec actively misleads consumers, arguably worse than having no formal documentation at all.
2. Treating "Swagger" and "OpenAPI" as fully interchangeable in technical writing/interviews
"We use Swagger for our API spec" // imprecise, likely means "we author an OpenAPI spec, possibly rendered via Swagger UI"While understood colloquially, this imprecision is worth avoiding in more technical contexts, Swagger today refers specifically to the tooling ecosystem, not the specification format itself.
3. Generating a client SDK from a spec and trusting its types without verifying the spec is current
const user = await client.getUser({ id: 42 });
user.email; // trusting generated types blindly, per Try It's exact failure modeA generated client's type safety is only as good as the spec it was generated from, if the spec can drift from reality (per Common Mistake #1), the generated types inherit that same risk, which is worth remembering before treating generated-SDK type safety as equivalent to tRPC's structurally-guaranteed variant.
Best Practices
- Generate the OpenAPI spec FROM code (framework annotations, or a shared schema library like zod also used for runtime validation) rather than hand-authoring it as an independent artifact.
- Use "OpenAPI" for the specification and "Swagger" for the tooling in precise technical communication, reflecting the actual current naming distinction.
- Run contract tests that verify the live API's actual behavior matches the OpenAPI spec, catching drift automatically rather than trusting a spec's accuracy indefinitely.
- Reference the same error-response shape (RFC 9457) in the OpenAPI spec's error responses as the API actually implements, keeping error documentation as accurate as success-path documentation.
- Choose OpenAPI specifically when cross-language client generation or public/third-party API documentation matters, for a same-team TypeScript-only setup, weigh it honestly against tRPC's simpler, drift-proof-by-construction alternative.
Performance Tips
- Generating an OpenAPI spec from code (versus hand-authoring) is a build-time cost, not a runtime one, it has zero production performance impact, making the "generate from code" recommendation essentially free from a performance standpoint.
- A well-structured OpenAPI spec enables generating client SDKs with efficient, typed request/response handling, the SDK generation step itself has real one-time cost (tooling setup, generated-code review), but the resulting client code's runtime performance is typically comparable to hand-written API-calling code.
