पाठ 23 / 27

OpenAPI के साथ Contract-First

API को OpenAPI दस्तावेज़ में वर्णित करें और उसे अपने आप lint व diff करें।

एक दस्तावेज़, कई उपयोग

OpenAPI दस्तावेज़ (YAML या JSON) paths, parameters, request व response schemas, errors और सुरक्षा को मशीन-पठनीय रूप में वर्णित करता है। इसे पहले डिज़ाइन करने (contract-first) से टीमें implementation से पहले API की समीक्षा और mock कर सकती हैं, और वही फ़ाइल फिर interactive docs, client SDK और server stub generation, request/response validation, contract tests और versions के बीच तोड़ने वाले बदलाव की जाँच चलाती है। components के अंतर्गत $ref से परिभाषाएँ दोबारा उपयोग करें, हर operation को अनोखा operationId दें, और error responses भी वर्णित करें। Spec को कोड के पास version control में रखें और implementation उससे भटके तो build विफल करें।

वादे को कोड में रखें

मशीन-पठनीय अनुबंध, स्वचालित जाँचें और साझा मानक कई टीमों को एकरूप रखते हैं।

तीन परतें: spec, tests, मानक।
चित्र 7.1 — Spec, tests और मानक।

छोटा OpenAPI 3.1 दस्तावेज़

v2 की list और get endpoints को पुनः उपयोग योग्य Item schema, problem+json error और rate-limit response के साथ वर्णित करता है। मैंने इसे अगले snippet में PyYAML से parse किया।

openapi: 3.1.0
info:
  title: Items API
  version: "2.0.0"
servers:
  - url: https://api.example.com
paths:
  /v2/items:
    get:
      operationId: listItems
      summary: List items (cursor pagination)
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        "200":
          description: A page of items
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ItemPage" }
        "429":
          $ref: "#/components/responses/RateLimited"
  /v2/items/{id}:
    get:
      operationId: getItem
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: One item
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Item" }
        "404":
          description: Not found
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
components:
  schemas:
    Item:
      type: object
      required: [id, first_name, last_name]
      properties:
        id: { type: integer }
        first_name: { type: string }
        last_name: { type: string }
        plan: { type: string, enum: [free, pro] }
    ItemPage:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Item" }
        next_cursor: { type: [string, "null"] }
    Problem:
      type: object
      required: [title, status]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
  responses:
    RateLimited:
      description: Too many requests
      headers:
        Retry-After: { schema: { type: integer } }

Spec की संरचना जाँचना, चलाकर

मैंने यह सादा-Python उदाहरण चलाया। ऊपर के YAML को openapi.yaml नाम से सहेजें; यह script उसे लोड करती है, operations सूचीबद्ध करती है, operationId की अनोखापन जाँचती है और पुष्टि करती है कि सभी 5 $ref संकेतक दस्तावेज़ के भीतर resolve होते हैं। यह सिखाने की जाँच है; असली projects समर्पित validators और linters उपयोग करते हैं।

import yaml
spec = yaml.safe_load(open("openapi.yaml"))
ops, refs = [], []
def walk(o):
    if isinstance(o, dict):
        if "$ref" in o: refs.append(o["$ref"])
        for v in o.values(): walk(v)
    elif isinstance(o, list):
        for v in o: walk(v)
walk(spec)
for path, item in spec["paths"].items():
    for method, op in item.items(): ops.append((method.upper(), path, op["operationId"]))
def resolve(ref):
    node = spec
    for part in ref.lstrip("#/").split("/"): node = node[part]
    return node
missing = [r for r in refs if not _ok(r)] if False else []
for r in refs:
    try: resolve(r)
    except KeyError: missing.append(r)
print(spec["openapi"], len(spec["paths"]), "paths")
print(ops)
print("unique operationIds:", len({o[2] for o in ops}) == len(ops), "| refs:", len(refs), "| unresolved:", missing)

Output:

3.1.0 2 paths
[('GET', '/v2/items', 'listItems'), ('GET', '/v2/items/{id}', 'getItem')]
unique operationIds: True | refs: 5 | unresolved: []

त्वरित जाँच: पहले OpenAPI अनुबंध डिज़ाइन करने का लाभ क्या है?

  • टीमें implementation से पहले समीक्षा, mock और कोड generate कर सकती हैं
  • यह किसी भी कोड की ज़रूरत हटाता है
  • यह APIs को धीमा करता है
  • यह सारे bugs रोकता है
Answer

टीमें implementation से पहले समीक्षा, mock और कोड generate कर सकती हैं — साझा, समीक्षा योग्य अनुबंध उत्पादकों और उपभोक्ताओं को जल्दी एकमत करता है।