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.

Quellinformationen

Repository
wso2/labs-agentic-engineer
Letzte Quellaktivität
4. Oktober 2026 um 10:37
Erkannte Sprache von SKILL.md
Englisch
Sterne
25
Forks
24

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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.
Auf GitHub ansehen