--- name: api-spec-openapi description: Generate OpenAPI 3.1 specs that follow the Zalando RESTful API Guidelines (kebab-case naming, cursor pagination, RFC 9457 problem+json errors, URL versioning, idempotency), one YAML file per bounded context/domain. Use when authoring, updating, or reviewing an OpenAPI/Swagger specification, or when preparing specs to push to SmartBear SwaggerHub. --- # OpenAPI 3.1 + Zalando REST API Guidelines Skill ## When to Activate - Authoring a new OpenAPI 3.1 specification from domain models - Updating an existing OpenAPI spec to align with Zalando conventions - Reviewing specs for API naming, error handling, pagination, versioning, or idempotency compliance - Preparing specs for publication to SmartBear SwaggerHub - Designing API contracts for a bounded context or microservice ## Core Rules Digest (Zalando-Derived) The highest-value Zalando rules are embedded below. Full checklist with verifiable YAML snippets: [zalando-checklist.md](zalando-checklist.md). ### **Naming** - Paths: **kebab-case**, lowercase, **plural resource nouns**, no trailing slash. ```yaml /v1/users /v1/market-searches/{id} /v1/order-items ``` - JSON properties: **snake_case** (not camelCase). ```yaml user_id: string created_at: string is_active: boolean ``` - **Identifiers MUST be English** — all path segments, property names, enum values, operationIds, and schema names MUST be English (e.g., `/customers/{id}/invoices`, not `/clients/{id}/factures`). See [zalando-checklist.md § English-only identifiers](zalando-checklist.md#english-only-identifiers). ### **HTTP Semantics** - **GET**: retrieve (200 OK or 404). - **POST**: create new resource (201 Created, Location header with URI). - **PUT**: replace entire resource, idempotent (200 or 204). - **DELETE**: remove resource, idempotent (204 No Content). - **PATCH**: partial update (not recommended; use PUT or POST instead). - **Status codes**: Use 200/201/202/204/207/400/401/403/404/409/412/422/429/500. See [zalando-checklist.md § HTTP Status Codes](zalando-checklist.md#http-status-codes) for mappings. ### **Pagination (Cursor-Based, No Offset)** - Cursor pagination: `?cursor=&limit=` (default limit 20, max 100). - **Forbid `?offset=` and `?page=`** — they are inefficient at scale. - Response envelope includes `_links.next` / `_links.prev` for navigation. ```yaml items: [...] _links: next: { href: "https://api.example.com/v1/users?cursor=abc123&limit=20" } prev: { href: "https://api.example.com/v1/users?cursor=xyz789&limit=20" } ``` ### **Errors: RFC 9457 Problem+JSON** - All 4xx/5xx responses use `application/problem+json` (RFC 9457 / RFC 7807). - Single reusable `Problem` schema across all endpoints. ```yaml type: string # URI identifying problem type (e.g., "https://api.example.com/problems/user-not-found") title: string # Human-readable summary status: integer # HTTP status code detail: string # Details specific to this occurrence instance: string # URI to affected resource ``` ### **Versioning (URL Major Only)** - Major versions encoded in URL: `/v1/`, `/v2/`. - **Preferred within-version evolution**: Additive only (no breaking changes within `/v1`). - Alternative: Media-type versioning (content negotiation `Accept: application/vnd.example.v1+json`); see [openapi-3.1-template.md](openapi-3.1-template.md). - **MUST NOT break** existing clients mid-version; bump major version if backward-incompatible. ### **Idempotency** - **POST (create)**: Require `Idempotency-Key` header (UUID or user-provided). - **PUT / DELETE**: Inherently idempotent; safe to retry. - Server returns same response if the same `Idempotency-Key` is submitted. ### **Filtering & Sorting** - Filtering: Query parameters per resource (`?user_id=123`, `?status=active`). - Searching: `?q=` for free-text search. - Sorting: `?sort=field,-field` (ascending by default, `-` prefix for descending). ### **Hypermedia & Links** - Include `_links` object in responses for navigation (cursor pagination, related resources, actions). - Standard link relations: `self`, `next`, `prev`, `first`, `last`. ### **Headers & Content Types** - Default: `application/json`. - Errors: `application/problem+json`. - Concurrency: `ETag` / `If-Match` for optimistic locking. - Retry: `Retry-After` on 429 (rate limit) and 503 (service unavailable). ### **Deprecation** - Use `Deprecation: true` header in responses. - Use `Sunset` header with a date when the endpoint will be removed. - Mark deprecated fields in schema with `deprecated: true`. ### **Security** - Define `securitySchemes` (e.g., OAuth2, Bearer token). - Apply security per operation (not globally) for granular control. - Never include secrets or credentials in examples or default values. ### **Tags (One Per Bounded Context)** - Assign one Title-Case English tag per bounded context. - Group operations under their originating BC's tag for clear domain organization. - Example tags: `"User Accounts"`, `"Billing"`, `"Order Fulfillment"`. - See [zalando-checklist.md § Tags](zalando-checklist.md#tags). ### **One File Per Bounded Context (or Consolidate if Needed)** - Default: Split the API specification by domain/bounded context. - Naming: `.openapi.yaml` in `api-specs/` folder (kebab-case). - Each BC spec is independently publishable to SwaggerHub; minimize cross-file `$ref`. - Alternative: Consolidate multiple BCs into a single API file; use tags to denote BC boundaries. - See [bounded-context-mapping.md](bounded-context-mapping.md). ## OpenAPI 3.1 Minimal Skeleton ```yaml openapi: 3.1.0 info: title: " API" version: "1.0.0" description: | RESTful API for . Follows Zalando RESTful API Guidelines and RFC 9457 for error responses. contact: name: "API Support" email: "api-support@example.com" license: name: "MIT" servers: - url: "https://api.example.com" description: "Production" tags: - name: "Users" description: "User management" - name: "Orders" description: "Order lifecycle" paths: /v1/users: get: summary: "List users" operationId: "listUsers" tags: ["Users"] parameters: - $ref: "#/components/parameters/CursorParam" - $ref: "#/components/parameters/LimitParam" responses: "200": description: "Users list" content: application/json: schema: $ref: "#/components/schemas/UserPage" "400": $ref: "#/components/responses/Problem400" "500": $ref: "#/components/responses/Problem500" security: - bearerAuth: [] post: summary: "Create user" operationId: "createUser" tags: ["Users"] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUserRequest" parameters: - $ref: "#/components/parameters/IdempotencyKeyParam" responses: "201": description: "User created" headers: Location: schema: type: string description: "URI of created resource" content: application/json: schema: $ref: "#/components/schemas/UserResponse" "400": $ref: "#/components/responses/Problem400" "409": $ref: "#/components/responses/Problem409" "422": $ref: "#/components/responses/Problem422" "500": $ref: "#/components/responses/Problem500" security: - bearerAuth: [] components: parameters: CursorParam: name: cursor in: query description: "Cursor for pagination" schema: type: string required: false LimitParam: name: limit in: query description: "Maximum items per page (default 20, max 100)" schema: type: integer minimum: 1 maximum: 100 default: 20 required: false IdempotencyKeyParam: name: Idempotency-Key in: header description: "Unique key for idempotent requests (UUID)" schema: type: string format: uuid required: true responses: Problem400: description: "Bad Request" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem401: description: "Unauthorized" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem403: description: "Forbidden" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem404: description: "Not Found" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem409: description: "Conflict" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem422: description: "Unprocessable Entity" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem429: description: "Too Many Requests" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" headers: Retry-After: schema: type: integer description: "Seconds to wait before retrying" Problem500: description: "Internal Server Error" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem412: description: "Precondition Failed" content: application/problem+json: schema: $ref: "#/components/schemas/Problem" Problem503: description: "Service Unavailable" headers: Retry-After: schema: type: integer content: application/problem+json: schema: $ref: "#/components/schemas/Problem" schemas: Problem: type: object required: - type - title - status properties: type: type: string description: "URI identifying problem type" example: "https://api.example.com/problems/user-not-found" title: type: string description: "Human-readable problem summary" example: "User Not Found" status: type: integer description: "HTTP status code" example: 404 detail: type: string description: "Details specific to this occurrence" example: "User with ID 'user-123' does not exist" instance: type: string description: "URI to the affected resource" example: "https://api.example.com/v1/users/user-123" CursorPage: type: object required: - items properties: items: type: array description: "List of items" _links: type: object properties: self: type: object properties: href: type: string next: type: object properties: href: type: string prev: type: object properties: href: type: string CreateUserRequest: type: object required: - email - name properties: email: type: string format: email name: type: string is_active: type: boolean default: true UserResponse: type: object required: - id - email - name - created_at properties: id: type: string email: type: string format: email name: type: string is_active: type: boolean created_at: type: string format: date-time updated_at: type: string format: date-time _links: type: object properties: self: type: object properties: href: type: string UserPage: allOf: - $ref: "#/components/schemas/CursorPage" - type: object properties: items: type: array items: $ref: "#/components/schemas/UserResponse" securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: "Bearer token (JWT)" ``` See [openapi-3.1-template.md](openapi-3.1-template.md) for full 3.1 idioms (JSON Schema 2020-12, examples, webhooks) and 3.0 migration notes. ## Error Model: RFC 9457 Problem+JSON Every API response with status 4xx or 5xx MUST use `application/problem+json` (RFC 9457 / RFC 7807). No ad-hoc error shapes. - Reusable `Problem` schema (shown above) covers all error responses. - Extensions: add custom fields as siblings to `type`, `title`, `status`, `detail`, `instance`. See [problem-json.md](problem-json.md) for full RFC 9457 model, .NET ProblemDetails mapping, and extension patterns. ## Validation Gate (MUST Run Before Done) Validators (Spectral, Redocly) MUST pass before the spec is declared done. ```bash # Spectral (recommended for Zalando ruleset) npx @stoplight/spectral-cli lint api-specs/*.openapi.yaml --ruleset path/to/zalando-ruleset.yaml # Redocly (OpenAPI 3.1 aware) npx @redocly/cli lint api-specs/*.openapi.yaml ``` If no validator runs, report it. See [validation.md](validation.md) for setup, fallback options, and invocation patterns. ## References - [zalando-checklist.md](zalando-checklist.md) — Concrete Zalando-derived checklist with verifiable YAML snippets. - [openapi-3.1-template.md](openapi-3.1-template.md) — OpenAPI 3.1 shape, JSON Schema 2020-12 idioms, 3.0 vs 3.1 differences, webhooks. - [bounded-context-mapping.md](bounded-context-mapping.md) — One file per BC convention, clustering heuristics, SwaggerHub cataloguing. - [problem-json.md](problem-json.md) — RFC 9457 model, extension pattern, .NET ProblemDetails example. - [validation.md](validation.md) — Spectral/Redocly tooling, invocation, fallback validation. --- **Key Takeaway**: Zalando specs are **predictable, discoverable, and client-friendly**. Kebab-case paths, snake_case JSON, cursor pagination, problem+json errors, URL versioning, and idempotency form a cohesive contract. One file per BC keeps specs independently publishable and reviewable.