Skip to main content

openapi-conventions

Use when creating or editing an openapi.yaml for a service component — designing endpoints, request/response schemas, errors, pagination, or security for a REST API.

Informations de source

Dépôt
wso2/labs-agentic-engineer
Dernière activité de la source
4 octobre 2026 à 10:37
Langue détectée de SKILL.md
anglais
Étoiles
25
Forks
24

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
2 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
openapi-conventions
description
Use when creating or editing an openapi.yaml for a service component — designing endpoints, request/response schemas, errors, pagination, or security for a REST API.
metadata
{"aep":{"kind":"platform","audience":["design","coding"]}}
# OpenAPI conventions Every `service` component gets one spec at `specs/design/components/<name>/openapi.yaml`, authored as **OpenAPI 3.0.3**. **The spec is validated as it lands.** A write to that path is rejected (`INVALID_OPENAPI`) unless the document is OpenAPI 3.x with at least one path and one operation, and a rejected write changes nothing. So a spec that applied cleanly has already passed that check: do not re-validate it with a separate tool. Handing your own spec back to a validator as pasted text costs a round trip and re-emits the entire document — for a check that already ran. Coverage is checklist-driven, not vibes: walk the PRD (`specs/requirements/prd.md`) against the component's `design.json` responsibility, and give every capability the requirements assign to THIS component its resource(s) and every core entity its schema. A capability with no endpoint is a defect. Commonly dropped when consolidating services: audit trail/logs, user & role management, notification preferences, reporting/analytics — check for each explicitly before finishing. **Keep the spec COMPACT.** Complete coverage, minimal prose: a short `summary` per operation and a one-line `description` per response — no multi-sentence descriptions, no `example`/`examples` blocks, no speculative endpoints the requirements don't imply. Schemas carry the required fields plus the few core properties that define the entity — not every conceivable attribute. A property whose name and type do not give its meaning, such as a unit, a direction or a sign, gets a one-line `description` in each schema that has it. For resource taxonomy (collection/atomic/controller), URI grammar, HTTP-method semantics, and a full worked example, read `references/wso2-rest-api-design-guidelines.md` — the source of truth this summary condenses. ## Structure - `servers:` is relative — `- url: /` — never an absolute external host. - Paths are **kebab-case plural nouns** (`/expense-claims`, `/expense-claims/{claimId}/line-items`); verbs only for controller actions (`/expense-claims/{claimId}/submit`). Max two nesting levels. - Every operation has `operationId` in **lowerCamelCase verb+resource** (`listExpenseClaims`, `submitExpenseClaim`) and a non-empty `summary`. - Every response has a non-empty `description`. Bodies are `application/json`. Reusable schemas live under `components/schemas`. ## Errors — one shared schema Define `components/schemas/Error` and reference it from EVERY 4xx/5xx response: ```yaml Error: type: object required: [code, message] properties: code: { type: integer, description: HTTP or application error code } message: { type: string, description: short human-readable label } description: { type: string, description: detailed explanation } moreInfo: { type: string, description: URI to documentation } ``` Each operation declares at least its failure modes: `'400'`/`'404'` where applicable, plus `'401'`/`'403'` when the API is authenticated. ## Pagination — every collection GET Parameters `limit` (integer, default 20, max 100) and `offset` (integer, default 0). The 200 response is an envelope, not a bare array: ```yaml type: object required: [count, data] properties: count: { type: integer, description: total matching items } next: { type: string, nullable: true, description: relative URI of the next page } previous: { type: string, nullable: true, description: relative URI of the previous page } data: { type: array, items: { $ref: '#/components/schemas/ExpenseClaim' } } ``` Filtering and searching are query parameters on the collection GET (`?status=submitted`, `?employeeId=...`) — never separate endpoints. ## Security **This block is the gateway's configuration, and the ONLY place the rule lives.** Deploy renders one gateway route per operation from it: the audience it pins and the single scope it requires. The service holds no copy of it — a request that fails this check never reaches the service at all. Get it right here and there is nothing else to configure anywhere. A component that depends on the sign-in resource type declares one scheme, one document-level default, and per-operation overrides: ```yaml components: securitySchemes: oauth2: type: oauth2 flows: authorizationCode: authorizationUrl: /oauth2/authorize tokenUrl: /oauth2/token scopes: claims:read: See own claims claims:submit: Create and send a claim security: - oauth2: [] # document default: any signed-in user ``` Every scope key, here and on an operation, is a handle from `specs/design/security.json` — `<resource>:<action>` — for a resource THIS component owns. Never invent one. `openid`, `profile`, `email`, `group` and `ou` are **reserved** OIDC scopes that ride every access token, so one of them on an operation would admit every signed-in person in the organisation — silently, and wide open. The gate refuses all five as an operation scope and as a `flows.*.scopes` key. **Three states, and only three.** Each operation is exactly one of: | `security` on the operation | Means | Gateway | Service | |---|---|---|---| | absent (inherits the document default) | any signed-in user | token required; signed assertion forwarded | 401 when the assertion is missing or does not verify | | `security: []` | public | no token checked; no assertion minted | reads no identity at all | | `security: [{oauth2: ["<handle>"]}]` | that permission | scope enforced, whole-string; 401 otherwise | no scope check — a request that arrives has passed it | The gateway answers **401** for every refusal (no token, expired, wrong audience, missing scope) with the same body and no `WWW-Authenticate`. No generated service answers 403: the only authorization question left to it is "is this row the caller's", and that answer is 404 (`api-management`). **Exactly one scope per operation.** No two-element list, no second scheme object, no `allOf`/`anyOf` question for the gateway and the service to answer differently. ## Reach is the path Which **rows** an operation reaches is its path, and nothing else says it: | Path | Reaches | Guarded by | |---|---|---| | `/me/<resource>` | the caller's own rows | the resource's own-rows action — `claims:read`, `claims:submit` | | `/me/<relation>/<resource>` | rows of a relation of the caller's — `/me/team/claims`, `/me/company/orders` | its own action — `claims:review-manager` | | anything else | **every row** | the every-row action — `claims:read-all`, `claims:approve` | The `<relation>` is a noun the domain model has (`team` because `Employee` has a `managerId`), never a role or a group name — `/me/team/claims`, not `/manager/claims`. Some operation writes that relation: if none sets `managerId`, `/me/team/claims` is empty for every caller. The handler behind a `/me/…` path resolves the rows through the gateway assertion's `sub` and nothing the client sends; a row that is not the caller's does not exist there, so the answer is 404, never 403. Two consequences, both mechanical: - **One reach per handle.** A handle guards operations under `/me/` or outside it, never both — the same grant cannot mean "your rows" on one operation and "every row" on another. The gate refuses the document that mixes them, naming both operations. A capability with two reaches is two operations with two handles: `GET /me/claims` on `claims:read`, `GET /claims` on `claims:read-all`. - **Nothing widens.** There is no handler-side check that turns one list into a bigger one. A caller who may see every claim calls `GET /claims`; the SPA picks the operation for the screen, and the gateway is the whole of the decision. A role that needs both views holds both handles. Say the reach in the operation's `summary` too — "The caller's claims", "Every claim" — so a reader of the contract does not have to parse the path for it. `bearerAuth` is not used on this platform. A component with no sign-in dependency declares no security scheme at all. **Declare no identity header.** The gateway hands the service the caller as a signed `x-jwt-assertion`; the unsigned `X-User-*` headers it also sets are not an authority and nothing reads them (`api-management`). Neither belongs in the contract: the assertion is a property of the deployment, not of the API, and a declared `X-User-Id` parameter invites a handler to bind one. The write gate refuses a `required: true` identity header and any identity header on a public operation; the correct document simply has none. ```yaml paths: /me/claims: get: summary: The caller's claims security: - oauth2: [claims:read] /claims: get: summary: Every claim security: - oauth2: [claims:read-all] /health: get: security: [] ``` **Never spec an auth endpoint.** No `/auth/login`, `/auth/register`, `/auth/logout`, or any other token-issuance path on any service: the IDP issues tokens and the gateway validates them (see `thunder-authentication`). Specifying one puts the coding agent's issue in direct conflict with its skills. ## YAML hygiene 2-space indentation throughout; quote status-code keys (`'200'`, `'404'`). The file is edited with anchored string edits later, so consistent indentation is load-bearing.
Voir sur GitHub