↓ Chuyển đến nội dung chính

API OpenAPI Dev

Lên kế hoạch API trước khi bắt tay xây dựng: quy trình OpenAPI-first

Quy trình lên kế hoạch bằng OpenAPI mà đội tôi áp dụng, để frontend và backend thống nhất hợp đồng API với nhau trước khi sprint bắt đầu, kèm một tệp YAML mẫu.

Cách đây không lâu, tôi có viết một bài nói chung chung về phát triển theo hướng OpenAPI-first. Bài này thì cụ thể hơn: đây là quy trình lên kế hoạch mà đội tôi đang chạy thử, kèm theo tệp YAML mẫu.

Chuyện này bắt nguồn từ đâu
#

Mọi chuyện bắt đầu từ một buổi họp nhìn lại (retrospective). Góp ý hôm đó khá thẳng thắn: gần như không ai nắm được khâu lên kế hoạch API đang diễn ra thế nào. Backend xây endpoint theo cách hiểu yêu cầu của riêng mình, frontend chỉ biết phản hồi (response) của API có cấu trúc ra sao khi mọi thứ đã xong xuôi, và những chỗ vênh nhau chỉ lộ ra lúc tích hợp, đúng thời điểm sửa tốn kém nhất. Đề xuất đưa ra là kéo cả hai bên vào ngay từ giai đoạn lên kế hoạch, và lấy bản đặc tả OpenAPI (OpenAPI schema) làm trục chính cho mọi thứ xoay quanh.

Quy trình
#

Mỗi tuần, chúng tôi chọn ra hai đại diện, một người bên backend và một người bên frontend. Nhiệm vụ của họ là nắm rõ yêu cầu của sprint và chỉnh bản đặc tả OpenAPI dùng cho khâu lên kế hoạch sao cho khớp với yêu cầu đó. Hai người cùng rà soát bản đặc tả, sửa những chỗ chưa khớp, và đến cuối buổi thì cả hai bên đều biết chính xác sẽ xây cái gì, trước khi có ai bắt tay vào làm.

Khi các chỉnh sửa đã chốt xong, bản đặc tả được commit vào kho mã (repo), và quy trình CI/CD sẽ sinh ra API giả lập (mock API) từ tệp yaml đó. Hết sprint, chúng tôi tạo một tệp yaml mới cho yêu cầu API của sprint kế tiếp, và vòng lặp cứ thế tiếp tục.

Ghi chú về công cụ
#

  • Muốn xem trực quan tệp yaml thì Swagger Editor là đủ: tải tệp lên là bạn sẽ thấy nó hiển thị thành một tài liệu Swagger đàng hoàng.
  • Bản đặc tả này chỉ phục vụ khâu lên kế hoạch. Khi API thật đã được triển khai, dịch vụ nên tự sinh ra openapi.json của riêng nó; bạn có thể tải tệp đó về rồi dùng lại cho vòng lên kế hoạch tiếp theo.
  • Quy trình CI/CD cần nhận vào một tệp yaml và dựng máy chủ giả lập (mock server) từ đó. Với việc này, openapi-mock trông có vẻ là một lựa chọn đáng tin.

Sao phải mất công như vậy
#

Vì làm vậy thì các cuộc tranh cãi sẽ diễn ra ở nơi rẻ nhất có thể. Khi có một kỹ sư frontend và một kỹ sư backend cùng ngồi viết bản đặc tả, những chỗ hiểu lầm sẽ được phát hiện trước khi kịp biến thành mã. Yêu cầu API chỉ cần thống nhất một lần, thay vì phải thương lượng lại giữa sprint. Nhờ có máy chủ giả lập, frontend bắt tay vào làm được ngay chứ không phải ngồi chờ. Và số chi tiết quan trọng bị bỏ sót cũng ít hơn hẳn, vì muốn lọt được thì chúng phải qua mắt hai người, lại còn phải qua một bản hợp đồng giấy trắng mực đen.

Chẳng có gì mang tính cách mạng ở đây cả. Chỉ là kế hoạch được làm cụ thể đến mức máy có thể dựng lại nó thành bản giả lập cho bạn dùng. Và hóa ra, đó cũng chính là mức cụ thể mà một bản kế hoạch cần có.

Phụ lục A - Ví dụ yaml
#

openapi: 3.1.0
info:
  title: PKY Sprint 3
  description: |-
    PKY Sprint 3 Development Schema
  termsOfService:
  contact:
    email: [email protected]
  license:
    name: None
    url: 
  version: 1.0.11
externalDocs:
  description:
  url: 
servers:
  - url: https://api-mock.com/api/v3
tags:
  - name: curation
    description: PKY-1081 Curated Lists Feature
    externalDocs:
      description: Jira Epic
      url: https://pickydev.atlassian.net/browse/PKY-1081
paths:
  /curations:
    get:
      tags:
        - curation
      summary: Return the curations created by users
      description: Returns a map of status codes to quantities
      operationId: getCurations
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: integer
                  format: int32
      security:
        - api_key: []
  /curations/{curationId}:
    get:
      tags:
        - curation
      summary: Find purchase order by ID
      description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
      operationId: getOrderById
      parameters:
        - name: orderId
          in: path
          description: ID of order that needs to be fetched
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
            application/xml:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          description: Invalid ID supplied
        '404':
          description: Order not found
    delete:
      tags:
        - curation
      summary: Delete purchase order by ID
      description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
      operationId: deleteOrder
      parameters:
        - name: orderId
          in: path
          description: ID of the order that needs to be deleted
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '400':
          description: Invalid ID supplied
        '404':
          description: Order not found
components:
  schemas:
    Curation:
      type: object
      properties:
        id:
          type: integer
          format: int64
          examples: [10]
        curationId:
          type: integer
          format: int64
          examples: [198772]
        quantity:
          type: integer
          format: int32
          examples: [7]
        status:
          type: string
          description: Order Status
          examples: [approved]
          enum:
            - placed
            - approved
            - delivered
        complete:
          type: boolean
      xml:
        name: order
    ApiResponse:
      type: object
      properties:
        code:
          type: integer
          format: int32
        type:
          type: string
        message:
          type: string
      xml:
        name: '##default'
  requestBodies:
    Curation:
      description: Curation object that needs to be added
      content:
        application/json:
          schema:
            $ref: 
        application/xml:
          schema:
            $ref:
  securitySchemes:
    petstore_auth:
      type: oauth2
      flows:
        implicit:
          authorizationUrl:
          scopes:
            write:curations: modify curations in your account
            read:curations: read your curations
    api_key:
      type: apiKey
      name: api_key
      in: header