When an API has no description, every developer writes requests by guesswork, client libraries are generated with cryptic names, and the documentation goes stale on day one. OpenAPI solves this: it describes the whole API in a single file that both humans and tools can read.
What OpenAPI is and why you need it
APIs used to be documented in Word files, Confluence pages, or purely "by word of mouth". The problem is obvious: the documentation lives separately from the code, goes stale, and lies.
OpenAPI is a standard for a machine-readable description of a REST API in YAML or JSON format. A single file describes every endpoint: URL, methods, parameters, request and response bodies, error codes.
What this gives you in practice:
- Swagger UI / Redoc — interactive documentation is generated automatically.
- Client SDK — openapi-generator produces a typed client in Java, TypeScript, Python, and other languages straight from the specification.
- Postman — request collections are imported from the file instead of being written by hand.
- Contract testing — tests verify that the implementation matches the specification.
A well-crafted OpenAPI file is not just documentation, it is the infrastructure around your API.
operationId: the operation name
Every endpoint in OpenAPI gets an operationId field. This is a unique operation name — it is exactly what SDK generators use as the method name.
Without an operationId, the generator invents a name itself. The result usually looks like postOrdersOrderIdConfirm — awkward to read and even worse to use.
Rule: unique, camelCase, format action + resource.
/api/v1/orders:
get:
operationId: getOrders # list
post:
operationId: createOrder # creation
/api/v1/orders/{orderId}:
get:
operationId: getOrder # single item
put:
operationId: updateOrder # full replacement
patch:
operationId: patchOrder # partial update
delete:
operationId: deleteOrder # deletion
/api/v1/orders/{orderId}/confirm:
post:
operationId: confirmOrder # action on a resource
This way the client code reads clearly: orderService.confirmOrder(orderId), rather than some cryptic auto-generated variant.
Naming convention:
get{Resource}— a single object (getOrder)get{Resources}— a list (getOrders)create{Resource}— creation via POSTupdate{Resource}— full replacement via PUTpatch{Resource}— partial update via PATCHdelete{Resource}— deletion{verb}{Resource}— business action (confirmOrder,cancelOrder)search{Resources}— search via POST with a body
tags: grouping endpoints
Without grouping, Swagger UI shows a flat list of a hundred endpoints — finding anything is impossible. The tags field solves this: endpoints are grouped into sections by resource.
Rule: one tag per resource, name in the plural, capitalized.
tags:
- name: Orders
description: 'Order management'
- name: Users
description: 'User management'
- name: Payments
description: 'Payments'
/api/v1/orders:
get:
tags: [Orders]
post:
tags: [Orders]
/api/v1/orders/{orderId}/confirm:
post:
tags: [Orders] # the action belongs to the parent resource's tag
A common mistake is to create a separate tag for actions: OrderActions, OrderOperations. This is not needed. The endpoint POST /orders/{orderId}/confirm belongs to the Orders tag, not to an invented Confirmations.
Path parameters: unique names in the specification
There is a subtlety here that surprises newcomers. In code and when designing URLs, people usually write {id} — the context is already clear from the path. But in OpenAPI you cannot do that:
# This doesn't work in Swagger UI — identical names within one path
/api/v1/orders/{id}/items/{id}:
get:
parameters:
- name: id # which id? the first or the second?
in: path
Swagger UI and Redoc cannot handle identical parameter names within a single path. That is why in an OpenAPI specification parameters are named uniquely:
/api/v1/orders/{orderId}/items/{itemId}:
get:
parameters:
- name: orderId
in: path
required: true
schema:
type: string
format: uuid
- name: itemId
in: path
required: true
schema:
type: string
format: uuid
This is a tooling requirement, not a semantic rule. In the API's own documentation and in code you can keep writing {id} — the context removes the ambiguity.
summary and description
summary is a short description of the endpoint, shown in Swagger UI next to the URL. The rule is simple: up to 80 characters, a phrase, not a sentence.
description is an extended explanation in Markdown format. Add it only when the endpoint's logic is non-obvious: special conditions, state transitions, constraints.
/api/v1/orders/{orderId}/confirm:
post:
operationId: confirmOrder
tags: [Orders]
summary: 'Confirm the order'
description: |
Moves the order from status CREATED to CONFIRMED.
The order must contain at least one item.
After confirmation the order's contents can no longer be changed.
An empty description is better than description: 'Confirm order' — a meaningless duplication of the summary.
Common mistakes in REST design
This is a breakdown of frequent problems encountered when designing and reviewing REST APIs.
Verb in the URL
# Wrong
GET /api/v1/getOrders
POST /api/v1/cancelOrder
POST /api/v1/orders/doConfirm
# Right
GET /api/v1/orders
POST /api/v1/orders/{id}/cancel
POST /api/v1/orders/{id}/confirm
In REST, the HTTP method already carries the meaning of the action. A verb in the URL is duplication and a violation of conventions. The only place where a verb in the URL is appropriate is business actions (/cancel, /confirm, /approve), and there it goes at the end of the path after the resource ID.
camelCase and snake_case in the path
URL paths are written in kebab-case (words separated by hyphens):
# Wrong
/api/v1/orderItems
/api/v1/order_items
# Right
/api/v1/order-items
Versioning
The version always goes in the path, and only in the path:
# Wrong
/api/orders?version=1
/api/orders?v=2
/2026/orders
# Right
/api/v1/orders
/api/v2/orders
Minor versions (v1.1, v1.2) are not needed — if a change does not break clients, the version does not change. If it does break them, that's v2.
Also: every path must start with /api/. This lets you separate the API from static content at the nginx or gateway level without conflicts.
ID in the request body instead of the path
// Wrong — PUT with the ID in the body
PUT /api/v1/orders
{ "id": "123", "status": "confirmed" }
// Right — ID in the path
PUT /api/v1/orders/123
{ "status": "confirmed" }
GET with a side effect
GET requests must be safe (idempotent): they can be repeated, cached, and logged without consequences. If a request changes something — use POST.
# Wrong
GET /api/v1/orders/123/cancel
# Right
POST /api/v1/orders/123/cancel
Query parameters: arrays and pagination
Arrays in query parameters are passed by repeating the parameter, not through a comma:
# Wrong
GET /api/v1/orders?status=NEW,CONFIRMED,DELIVERED
# Right
GET /api/v1/orders?status=NEW&status=CONFIRMED&status=DELIVERED
Zero-based pagination confuses API users: page=0 means "the first page", but looks like "the zeroth". In public contracts, use page=1 for the first page.
Responses: envelope and null
An envelope is a wrapper like { "success": true, "data": {...} }. It seems convenient, but it breaks standards: the HTTP code already tells you about success or failure, data adds an extra level of nesting, and clients have to unwrap the wrapper every time.
// Wrong — envelope
{ "success": true, "data": { "id": "123", "status": "CONFIRMED" } }
// Right — a flat resource
{ "id": "123", "status": "CONFIRMED" }
Fields with a null value are better left out of the response entirely — an absent field and null carry different meaning, and clients often cannot tell them apart. An empty string "" should not be used for "no value" either — only the absence of the field.
Errors
Errors must be returned in the RFC 9457 (Problem Details) format with content-type application/problem+json:
{
"type": "urn:problem:orders:order-not-found",
"title": "Not Found",
"status": 404,
"detail": "Order 123 not found"
}
Common problems:
type: "about:blank"— a meaningless placeholder type that carries no information- stack traces in a 500 response — leaking implementation details to the outside; the client needs a
codeand a generaldetail, not a stack trace application/jsoninstead ofapplication/problem+json— violates the standard
Headers and deprecation
Custom headers should not start with X- — this prefix is officially deprecated. Use a domain prefix:
# Deprecated approach
X-Request-Id: abc123
X-Rate-Limit: 100
# Right
Shop-Request-Id: abc123
RateLimit-Limit: 100
If an endpoint is being marked as deprecated — add a Sunset header with the shutdown date. Without it, clients do not know when to stop using it.
When rate-limiting requests (429 Too Many Requests), return Retry-After — the client needs to know when it can retry the request.
In short
- OpenAPI describes the whole API in a single file — documentation, SDKs, and tests are generated from it.
operationId— unique, camelCase, formataction + resource(createOrder,confirmOrder).tags— one tag per resource, plural and capitalized (Orders,Users); actions belong to the parent resource's tag.- In OpenAPI, path parameters are named uniquely (
{orderId},{itemId}) — a Swagger/Redoc requirement. summary— up to 80 characters;description— only if the logic is non-obvious.- A verb in the URL for CRUD is a mistake; business actions go at the end of the path (
/confirm,/cancel). - Paths in kebab-case, version in the path (
/api/v1/), arrays by repeating the parameter. - An envelope (
success/data) is not needed — use a flat resource and HTTP codes. - Errors are returned in the
application/problem+jsonformat without stack traces. - Custom headers without
X-; deprecated endpoints — with aSunsetheader.
What to read next
- URLs and resources in REST — rules for naming paths, nesting, resources.
- HTTP methods and status codes — when to use which method and status code.
- API versioning — v1/v2 strategies, backward compatibility.
- Errors and Problem Details — RFC 9457 in practice.