Keep your API stable as it grows. Version it so changes do not break existing clients, then describe it with an OpenAPI spec that doubles as living documentation.
Once other people build against your API, you cannot freely change it — renaming a field or removing one breaks every client that depended on it. Versioning gives you room to evolve: you publish v1, and when you need an incompatible change you ship v2 alongside it while old clients keep using v1. The most common style is a version segment in the URL path. Only the truly breaking changes need a new version; adding a new optional field is safe and needs no bump.
// Put the version in the path. app/api/v1/users/route.ts -> /api/v1/users
GET /api/v1/users/42
{ "id": "42", "name": "Ada Lovelace" }
// Later you need a breaking change (split name into two fields).
// Ship it as v2; v1 keeps working untouched for existing clients.
GET /api/v2/users/42
{ "id": "42", "firstName": "Ada", "lastName": "Lovelace" }
// Safe (no new version needed): ADDING an optional field.
// Breaking (needs a new version): renaming, removing, or changing
// the type of an existing field.OpenAPI is a standard format for describing a REST API in a single file — every endpoint, what it accepts, and what it returns. Why bother? That one file becomes interactive documentation (with tools like Swagger UI), generates client code in many languages, and powers automated tests — all from a single source of truth, so the docs cannot drift away from the real API. It is usually written in YAML, a plain-text format where indentation shows structure.
# openapi.yaml — a minimal but complete description of one endpoint
openapi: 3.1.0
info:
title: Users API
version: 1.0.0
paths:
/users/{id}:
get:
summary: Get one user by id
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
'200':
description: The user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: No user with that id
components:
schemas:
User:
type: object
properties:
id: { type: string }
name: { type: string }Documentation rots the moment it lives far from the code. The trick that keeps OpenAPI honest: derive it from the same schemas you already validate with. Because you defined your request and response shapes with Zod in earlier lessons, you can turn those into OpenAPI automatically — so the docs are generated from the exact rules the API enforces, and the two can never disagree.
$ pnpm add zod-to-openapiimport { z } from 'zod'
import { extendZodWithOpenApi } from 'zod-to-openapi'
extendZodWithOpenApi(z)
// The SAME schema validates requests AND describes them in the docs.
export const User = z
.object({
id: z.string().openapi({ example: '42' }),
name: z.string().openapi({ example: 'Ada Lovelace' }),
})
.openapi('User')
// A build step collects these schemas into the openapi.yaml above.
// Change the schema once -> validation and documentation both update.