Open a generated SDK where the methods are named getV1UsersByIdGet, postV1UsersPost, and usersGet2, and the cost of careless operationId s is immediate: nobody can discover anything, and every rename is a breaking change for downstream code. Open the docs sidebar and see 200 operations in one flat list because every endpoint got its own tag, and the same problem appears on the human side. These two fields look like optional labels; they are the public naming API of your service.
| Field | Consumed by | Consequence of getting it wrong |
|---|---|---|
operationId |
Code generators, mocking tools, agent tool lists | Becomes the method/function name; must be unique and stable |
tags |
Documentation navigation, generator namespaces | Groups operations into sections or SDK classes |
summary /description |
Docs, AI tool descriptions | The sentence a developer or agent reads first |
x-* group names |
Some renderers | Vendor-specific grouping; do not rely on it portably |
Generators that do not find an operationId synthesize one from the method and path, which is how you get getV1UsersByIdGet. Once a synthesized name ships, fixing it later breaks every caller. Set the id deliberately from day one.
Unique across the whole document. Two operations cannot share an id; some generators dedupe by appending a number, silently. Enforce uniqueness with a linter.
Language-neutral and code-safe. Use lowerCamelCase with ASCII letters and digits, starting with a letter. Avoid hyphens, dots, and spaces, because not every language maps them cleanly into an identifier.
Verb-first, resource-oriented, specific. The id should read as an action on a resource and say enough to be unambiguous without the path:
| Method and path | Avoid | Prefer |
|---|---|---|
| GET /users | getUsersGet |
listUsers |
| POST /users | postUsers |
createUser |
| GET /users/{id} | getUserById (fine) orgetV1UsersId |
getUser |
| PATCH /users/{id} | updateUser (ambiguous vs PUT) |
patchUser /updateUser split by verb |
| DELETE /users/{id} | deleteUsersId |
deleteUser |
| POST /users/{id}/archive | archive |
archiveUser |
| GET /users/{id}/orders | getOrders |
listUserOrders |
A consistent verb vocabulary removes the guesswork: list for collections, get for one, create, update/ patch, delete, plus domain actions like archive, cancel, approve, export. Avoid generic verbs like process or handle that say nothing.
Stable forever once published. The id is part of the contract because generated code embeds it. Renaming it is a breaking change for SDK users even if the URL does not change. Treat it like a field name: pick it once, lint it, and never reuse a retired id for a different operation.
Do not encode the version or HTTP method. getV2Users bakes the version into the method, so a future v3 forces a rename even for clients that never cared. Version belongs in the server URL or path, not the id.
Tags group operations in docs and often become SDK classes or namespaces. The failure modes are a tag per endpoint (no grouping at all) and a single tag for everything (one giant list). Aim for a small, stable set aligned to business domains, not to URLs:
tags:
- name: Users
description: Customer accounts, profiles, and authentication identities.
- name: Orders
description: Order lifecycle, line items, and status transitions.
- name: Billing
description: Invoices, payment methods, and refunds.
- name: Webhooks
description: Event subscriptions and delivery logs.
Rules that keep the taxonomy usable:
description; do not rely on ad-hoc names appearing only on operations (typos then create duplicate tags like User and Users). Webhooks, but three or more tags per operation scatter it across the docs.Billing, Orders), not internal service names ( payment-svc) or HTTP concepts.
If you need finer grouping than a tag gives, use x-tagGroups (supported by several renderers) to cluster tags into sections like "Catalog" and "Account" without multiplying tags.
summary is a short imperative label; description is the detail. For an AI agent that turns operations into callable tools, the summary and the first line of the description are often the entire basis for choosing the operation. Write them to disambiguate:
operationId: listUserOrders
summary: List a user's orders
description: >-
Returns orders belonging to the given user, newest first. Supports cursor
pagination and filtering by status. Does not include line items; use
getOrder for those.
That one sentence ("does not include line items; use getOrder") prevents a class of wrong agent calls that a name alone cannot.
Naming conventions are exactly the kind of rule a linter should guarantee rather than a wiki page hoping people remember. A few high-value rules:
operationId matching an allowed verb prefix;
Run the ruleset in CI so a PR that introduces postV2ThingsPost fails before merge, and add a check that flags a brand-new tag, forcing a conscious decision to grow the taxonomy.
client.users.list() style APIs; a missing id yields path-derived noise.listUserOrders over listOrders correctly. Ambiguous or missing ids make the agent guess, and the guess lands in production calls.
Get these right and the generated SDK reads like an API a human designed, the docs navigate cleanly at 200 routes, and an AI agent picks the right operation without guessing.
You can apply these conventions, generate a cleanly named TypeScript client, and lint the result all in one local-first workspace, right in your browser. To see how the ids and tags surface in a generated SDK, read generating a TypeScript client from OpenAPI.