Index Ai Academy
Phase 8 · APIs · Design at scale

Documenting APIs with OpenAPI

A machine-readable contract for docs, SDKs and AI tools.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Documentation written by hand drifts from the code within weeks. OpenAPI is a description of an API that machines can read, and tools build almost everything from it: interactive docs, client libraries in many languages, tests, mock servers. The official Anthropic and OpenAI SDKs are generated from OpenAPI specs. It also makes an API usable by AI, because each operation maps neatly onto a tool definition for a model, or onto an MCP server.

What you'll be able to do

  • Read an OpenAPI document: paths, operations, parameters, bodies and responses
  • Generate a spec from your routes, the way FastAPI does
  • Turn API operations into tool definitions an LLM agent can call
  • Use a spec for docs, generated SDKs, contract tests and spotting breaking changes

What it covers

  1. One document describes the whole API
  2. What a spec gives you
  3. Code first or design first
  4. Generating paths from a route table
  5. From OpenAPI to LLM tools
  6. An operation becomes a tool
  7. A spec for agents isn't a spec for people

One document describes the whole API

Think of a restaurant menu. It lists every dish, the choices you can make and what you'll get. An OpenAPI document (formerly called Swagger) does that for an API, written in JSON or YAML, which is a tidier way to write the same structure. Its paths section lists every endpoint, and under each, its methods, called operations:

{
  "openapi": "3.1.0",
  "info": {"title": "Shop API", "version": "1.4.0"},
  "paths": {
    "/campaigns/{campaign_id}": {
      "get": {
        "operationId": "get_campaign",
        "summary": "Get one campaign by id.",
        "parameters": [
          {"name": "campaign_id", "in": "path", "required": true, "schema": {"type": "string"}}
        ],
        "responses": {
          "200": {"description": "The campaign"},
          "404": {"description": "No such campaign"}
        }
      }
    }
  },
  "components": {
    "securitySchemes": {"apiKey": {"type": "http", "scheme": "bearer"}}
  }
}

Here's how to read it:

  • operationId is a unique name for the operation, like a dish's number on the menu. Code generators turn it into a method name, so client.get_campaign(...).
  • parameters say where each value goes: "in": "path", "query" or "header", with a JSON Schema for each.
  • requestBody holds the body's schema. responses maps status codes to what comes back.
  • components stores reusable pieces: shared schemas, and how authentication works.

Since version 3.1, the schemas inside are plain JSON Schema, the same language as your validator and Claude's tool input_schema.

Quick check
In OpenAPI, how does a parameter say it comes from the query string, like ?status=active?
  1. "in": "path"
  2. By starting its name with ?
  3. "location": "url"
  4. "in": "query"
Show the answer

"in": "query"

"in" can be path, query, header or cookie. Path parameters are always required.

What a spec gives you

An architect's drawing does a lot of jobs. The builder, the electrician and the inspector all work from the same sheet. A spec, the OpenAPI document for your API, is that sheet:

  • Interactive docs. Swagger UI and Redoc render the spec as browsable pages with "try it" buttons.
  • Client SDKs. Generators such as openapi-generator and Stainless write typed client libraries in many languages, with retries and pagination built in.
  • Validation. A server can check every request against the spec's schemas.
  • Mock servers. Front-end teams can build against fake responses before the API exists.
  • Contract tests. Tests check that real responses match the documented schemas.
  • Breaking-change checks. Comparing two versions of the spec in CI, the automatic checks that run on each change, flags a removed endpoint or a new required field before it ships. oasdiff is one tool for this.
  • AI tools. Each operation can become a tool for an LLM, as you'll build below.

In plain words: write one accurate description of your API, and let tools produce the rest from it.

Try it now

One accurate document does the job of five that would drift apart.

Code first or design first

You can build a shed and sketch it afterward, or draw it first and show the family before you buy any wood. APIs offer the same choice:

  • Code first: write the API, and generate the spec from it. FastAPI does this from your type hints and serves it at /openapi.json, with docs at /docs.
  • Design first: write the spec, review it with the people who'll call the API, then implement it and test against it.

Code first is faster for small teams. Design first catches design mistakes while they're still cheap to fix. Either way, the spec lives in git and changes are reviewed like code.

Quick check
What can a team generate from one OpenAPI spec?
  1. Only a documentation page
  2. Interactive docs, client libraries, mock servers and contract tests
  3. The production server's infrastructure
  4. The database schema
Show the answer

Interactive docs, client libraries, mock servers and contract tests

One machine-readable description feeds many tools, which is why teams keep the spec accurate and checked in CI.

Worked example

Generating paths from a route table

A framework already knows every route. Turning that list into OpenAPI paths takes a few lines. Path parameters are found from the braces in the pattern.

import json, re

routes = [
    {"method": "GET", "path": "/campaigns", "operation_id": "list_campaigns", "summary": "List campaigns."},
    {"method": "GET", "path": "/campaigns/{campaign_id}", "operation_id": "get_campaign", "summary": "Get one campaign."},
]

paths = {}
for r in routes:
    path_params = re.findall(r"{(\w+)}", r["path"])
    paths.setdefault(r["path"], {})[r["method"].lower()] = {
        "operationId": r["operation_id"],
        "summary": r["summary"],
        "parameters": [{"name": p, "in": "path", "required": True, "schema": {"type": "string"}} for p in path_params],
    }
print(json.dumps(paths, indent=1)[:600])
It runs right here in your browser, with nothing to install.

Every route becomes an operation under its path, with the method in lowercase as OpenAPI expects. re.findall pulls the names out of {...} in the pattern. The exercise adds query parameters, request bodies and responses.

From OpenAPI to LLM tools

A tool definition for Claude has a name, a description and an input_schema. An OpenAPI operation has an operationId, a summary and parameter schemas. It's like copying a menu into another format: the dishes stay the same, and only the labels change. The match is almost one to one:

OpenAPIClaude tool
operationIdname
summary / descriptiondescription
parameters and request body schemasinput_schema.properties
required parametersinput_schema.required

When the model calls the tool, your code turns its input back into a request: path parameters into the URL, query parameters into the query string, the body into JSON. This is how agents get access to a whole API at once, and how many MCP servers are generated.

Two cautions. First, descriptions written for developers aren't always good for models: a summary like "Get campaign" says nothing about when to use it. Second, don't expose every operation. An agent with a delete_all_campaigns tool will eventually use it. Pick a safe, useful subset.

Worked example

An operation becomes a tool

An agent needs a name, a description and an input schema for each tool. A good spec already holds all three, so the conversion is a few lines.

import json

spec = {"paths": {"/products/{id}": {"get": {
    "operationId": "getProduct",
    "summary": "Fetch one product by id",
    "parameters": [{"name": "id", "in": "path", "required": True, "schema": {"type": "integer"}}],
}}}}

def to_tools(spec):
    tools = []
    for path, methods in spec["paths"].items():
        for method, op in methods.items():
            params = op.get("parameters", [])
            tools.append({
                "name": op["operationId"],
                "description": op["summary"],
                "input_schema": {
                    "type": "object",
                    "properties": {p["name"]: p["schema"] for p in params},
                    "required": [p["name"] for p in params if p.get("required")],
                },
            })
    return tools

print(json.dumps(to_tools(spec)[0], indent=1))
In the lesson you can change this and run it.

The summary becomes what the model reads when it's choosing a tool. So a vague summary in your spec becomes a vague tool. Writing good specs now also helps every agent that uses your API later.

Quick check
What does this print?
op = {"operationId": "getItem", "summary": "Fetch one item"}
tool = {"name": op["operationId"], "description": op["summary"]}
print(tool["name"])
  1. name
  2. getItem
  3. Fetch one item
  4. operationId
Show the answer

getItem

tool["name"] holds op["operationId"], which is getItem. A good spec maps directly onto a tool definition.

Quick check
You convert a 300-operation OpenAPI spec into 300 tools for an agent. What's the main risk?
  1. Nothing; more tools always help
  2. Claude can't read JSON Schema
  3. The API gets faster
  4. Too many tools crowd the context and confuse the model, and dangerous operations become one call away
Show the answer

Too many tools crowd the context and confuse the model, and dangerous operations become one call away

Every tool definition costs tokens and adds a choice. Expose a curated, safe subset, with descriptions that say when to use each tool.

A spec for agents isn't a spec for people

Generating tools from a spec is quick, and it's easy to overdo. Two problems come up every time.

Don't expose every operation. A new assistant doesn't need every key on the company's key ring. An API with 200 endpoints makes 200 tools. Tool definitions cost tokens on every call, and the model chooses worse from a long list. It may also pick something dangerous, like a delete, when all it needed was to look. Choose the few operations an agent needs, and combine several into one where that fits the task.

Rewrite descriptions for the model. "Retrieves the resource" is fine in docs for a developer, who can read around it. A model decides what to call from that text alone, so say when to use the tool, what comes back, and what it doesn't do.

Keep the spec honest

A map is only useful if the roads still match it. Docs written by hand drift away from the real API, usually within weeks. So generate one from the other, or check them against each other in CI. If your code is the source, generate the spec from it. If the spec is the source, run tests that compare real responses against it.

A spec also lets a tool catch breaking changes automatically. Renaming a field, or making an optional one required, breaks clients that are already running. Tools such as oasdiff compare the old spec and the new one and fail the build, so you find out before your users do.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

What is operationId used for?

  1. A unique name for an operation, used for generated method names and tool names
  2. The HTTP status code that the operation returns when a call succeeds
  3. The database table name that the operation reads from and writes to
  4. The API key that a client has to send in order to call the operation

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson