Works with the AI Agents you already use

    Claude CodeClaude CodeCursorCursorCodex CLICodex CLIMuseMuseDotsDots+22 more

    API Design — Contract-First REST, Errors & Versioning

    1

    Contract-first HTTP API design and review: resources, status codes, pagination, errors, idempotency, versioning.

    Free

    Security scanned
    API Design — Contract-First REST, Errors & Versioning

    API Design — Contract-First REST, Errors & Versioning

    Example session with this skill installed

    We're about to build the orders API for our shop. Mobile app and two partner integrations will consume it. Can you review this draft before we write any code?

    POST /api/createOrder
      body: { "userId": 123, "items": [{ "sku": "TSHIRT-M", "qty": 2 }], "cardToken": "tok_9f..." }
      -> 200 { "success": true, "order": { "id": 48213, "user_id": 123, "status_cd": 1, "total": 59.8 } }
    
    GET /api/getOrders?userId=123&apiKey=sk_live_...
      -> 200 [ ...every order the user ever placed... ]
    
    POST /api/orders/cancel
      body: { "orderId": 48213 }
    
    Errors: always 200 with { "success": false, "error": "<message>" }
    IDs are the Postgres auto-increment primary keys.
    
    • Read your context and instructions
    • Compiled the api design

    Summary. REST fits here. The consumers are a mobile app and outside partners, and the work is resource CRUD over orders, so broad client reach matters more than flexible queries. The draft has 9 problems. Six of them, marked blocker, would be expensive or unsafe to fix after partners integrate. Nothing has shipped yet, so every fix below is free now. The same changes on a live API would be breaking and would need a new version.

    Findings

    #SeverityIssueRuleFix
    1BlockerapiKey in the query stringNo secrets in URLs (they end up in logs, history and Referer)Send Authorization: Bearer <token> in a header
    2BlockeruserId is supplied by the client in the body and the queryObject-level authorization (BOLA, OWASP API #1)Get the caller from the validated token. GET /orders/{id} must check that the order belongs to the caller
    3BlockerPOST /api/createOrder has no idempotency keyWrites must be safe to retryRequire an Idempotency-Key header, store the first response and replay it on retry
    4BlockerGET /api/getOrders returns every orderPaginate every list from day oneCursor pagination with page_size (default and hard max) and page_token
    5BlockerErrors return 200 with success:falseStatus code carries the outcomeRFC 9457 application/problem+json with the matching status and a stable code
    6BlockerAuto-increment IDs (48213) are exposedOpaque IDsOpaque string IDs (ord_…). Clients must not parse them
    7MajorVerbs in paths (createOrder, getOrders, cancel)Resources are nounsPOST /orders, GET /orders, POST /orders/{id}/cancellations
    8MajorDB columns leak into the response (status_cd, user_id) and money is a floatDon't expose internal modelsA deliberate DTO: status as a documented enum, money in integer minor units plus currency
    9MinorNo versioning, and /api prefix is used with mixed casingConsistent conventions, explicit versioning/v1/ path versioning, kebab-case paths, one casing for JSON fields

    Proposed contract (OpenAPI excerpt, write this before the handlers)

    openapi: 3.1.0
    info: { title: Orders API, version: "1" }
    paths:
      /v1/orders:
        post:
          summary: Create an order for the authenticated customer
          parameters:
            - { name: Idempotency-Key, in: header, required: true, schema: { type: string, maxLength: 255 } }
          requestBody:
            content:
              application/json:
                schema: { $ref: "#/components/schemas/CreateOrder" }
          responses:
            "201": { description: Created, headers: { Location: { schema: { type: string } } } }
            "401": { $ref: "#/components/responses/Problem" }
            "409": { $ref: "#/components/responses/Problem" }
            "422": { $ref: "#/components/responses/Problem" }
        get:
          summary: List the authenticated customer's orders
          parameters:
            - { name: page_size, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
            - { name: page_token, in: query, schema: { type: string } }
            - { name: status, in: query, schema: { $ref: "#/components/schemas/OrderStatus" } }
          responses:
            "200": { description: A page of orders with next_page_token }
      /v1/orders/{order_id}:
        get:
          responses:
            "200": { description: The order (only if it belongs to the caller), headers: { ETag: { schema: { type: string } } } }
            "404": { $ref: "#/components/responses/Problem" }
      /v1/orders/{order_id}/cancellations:
        post:
          parameters:
            - { name: If-Match, in: header, required: true, schema: { type: string } }
          responses:
            "201": { description: Cancellation recorded }
            "409": { description: Order can no longer be canceled (already shipped) }
            "412": { description: Order changed since it was read }
            "428": { description: If-Match missing }
    

    CreateOrder holds items[] (sku, qty with a max), currency and a payment_method_id. It has no user_id, and fields that aren't on the allow-list are rejected, which blocks mass assignment.

    Error envelope (every error path)

    {
      "type": "https://api.example.com/errors/out-of-stock",
      "title": "Item out of stock",
      "status": 422,
      "detail": "SKU TSHIRT-M has 1 unit left, 2 requested.",
      "code": "OUT_OF_STOCK",
      "errors": [{ "field": "items[0].qty", "code": "EXCEEDS_STOCK" }],
      "traceId": "0af7651a..."
    }
    

    Stack traces, SQL and class names are logged on the server under traceId and are never returned to the client.

    Decisions to confirm

    Card data. cardToken in the order body suggests payment happens inside order creation. If you keep it, the idempotency key is what prevents a double charge on retry. Confirm which provider token format you are accepting.

    Cancel as a sub-resource or a state change. POST /orders/{id}/cancellations keeps a record of who canceled and why. PATCH /orders/{id} {"status":"canceled"} also works if no audit record is needed.

    Rate limiting. Return 429 with Retry-After per token, and use an explicit CORS allow-list if a web client is ever added.

    Next steps

    1. Write the full openapi.yaml from the excerpt and review it with the mobile and partner teams before any handler exists.
    2. Add a spec linter (for example Spectral) to CI so drift between spec and code fails the build.
    3. Implement handlers against the spec. Put the per-order ownership check in one place that every /{order_id} route goes through.
    4. Learning capture: nothing project-specific came up beyond the defaults, so no learnings/ entry was written.

    Connects securely to your tools. The creator never sees your data.

    About this skill

    API Design is a workflow skill for designing a new API or reviewing a change to one. It treats the API as a contract that has to keep evolving without breaking the clients that already depend on it.

    Problems it solves

    • Verbs in paths (POST /createUser), 200 OK with an error in the body, and casing that changes from one endpoint to the next.
    • List endpoints that return everything, and pagination added later in a way that silently breaks clients.
    • POST requests that create duplicates when a client retries after a timeout.
    • Missing object-level authorization (BOLA, OWASP API #1), secrets in URLs, and stack traces in error responses.

    How it works. It is an 11-step workflow, and each step ends with a check you can verify. You choose the protocol on purpose (REST, GraphQL or gRPC), write the OpenAPI spec before the code, model resources as nouns, use HTTP methods and status codes as specified, and classify every change as additive or breaking. Errors use RFC 9457 problem+json with a stable code. Every list is paginated by cursor from day one. Writes are safe to retry with Idempotency-Key and ETag/If-Match. Security is on by default. The skill also lists anti-patterns to reject on sight.

    References, loaded only when needed

    • rest-conventions.md: naming, methods, status codes, pagination, filtering, caching.
    • versioning-and-compatibility.md: versioning strategies, additive vs breaking changes, deprecation with Deprecation/Sunset headers.
    • errors-idempotency-concurrency.md: problem+json, idempotency keys, optimistic concurrency, retries, long-running operations, webhooks.
    • security.md: TLS, OAuth2/OIDC, BOLA/BFLA, mass assignment, rate limiting, CORS.

    When it triggers. It activates on tasks that touch an API: adding an endpoint, choosing a status code, paginating, versioning, modeling errors, webhooks or OpenAPI. It reads a project learnings/ folder first, and your own conventions there override the defaults.

    Free and MIT-licensed. Part of the claude-skills-czl collection.

    Source on GitHub: this skill · full repository

    How to use

    Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.

    Reviews

    No reviews yet

    Be one of the first to try it. Every listed skill passes our trust checks below.

    Security scanned

    Passed our 8-point scan before listing

    Fresh listing

    Recently published to Agensi

    Free forever

    No account required to browse

    Trust & safety

    Security scanned

    Verified clean 11 days ago

    • Free to download with an account

    Listed11 days ago

    What's inside

    Frequently Asked Questions