OpenAPI 3.1 + Zalando REST API Guidelines Skill

SkillFiles & storage

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the OpenAPI 3.1 + Zalando REST API Guidelines Skill skill

What this skill tells your AI

The instructions your AI receives, as published by fmflurry/settings-opencode in skills/api-spec-openapi/SKILL.md and read by ahel’s review.

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.

Naming

  • Paths: kebab-case, lowercase, plural resource nouns, no trailing slash.
    /v1/users
    /v1/market-searches/{id}
    /v1/order-items
    
  • JSON properties: snake_case (not camelCase).
    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.

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 for mappings.

Pagination (Cursor-Based, No Offset)

  • Cursor pagination: ?cursor=<opaque-value>&limit=<int> (default limit 20, max 100).
  • Forbid ?offset= and ?page= — they are inefficient at scale.
  • Response envelope includes _links.next / _links.prev for navigation.
    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.
    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.
  • 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=<query> 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.

One File Per Bounded Context (or Consolidate if Needed)

  • Default: Split the API specification by domain/bounded context.
  • Naming: <bounded-context>.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.

OpenAPI 3.1 Minimal Skeleton

openapi: 3.1.0
info:
  title: "<Bounded Context> API"
  version: "1.0.0"
  description: |
    RESTful API for <bounded context description>.
    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 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 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.

# 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 for setup, fallback options, and invocation patterns.

References


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.

Signals

GitHub stars
171
Forks
10
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
api-spec-openapi
Source
github.com/fmflurry/settings-opencode