Bỏ qua

Tiêu chuẩn API

Quy chuẩn Thiết kế API cho Hệ thống EZD AI Booth

Tất cả các file đặc tả OpenAPI (.yaml) trong dự án BẮT BUỘC phải tuân thủ các quy chuẩn dưới đây.

1. Metadata (info block):

  • Mỗi file phải có title, description, và version rõ ràng.
  • contact phải được điền đầy đủ thông tin của đội ngũ kỹ thuật.

2. Server Definitions (servers block):

  • BẮT BUỘC: Mỗi file đặc tả phải định nghĩa ít nhất 2 server: productionstaging (hoặc development).

3. API Versioning:

  • BẮT BUỘC: Version của API phải được thể hiện rõ trong URL, theo dạng /v1/....

4. Gắn thẻ (tags):

  • Mọi path (endpoint) phải được nhóm vào một tag có ý nghĩa (ví dụ: Promotions, FAQs, Authentication) để dễ dàng điều hướng trên giao diện Swagger UI.

5. Tái sử dụng Schema (components/schemas):

  • KHÔNG định nghĩa schema (cấu trúc dữ liệu) trực tiếp trong requestBody hoặc responses.
  • BẮT BUỘC phải định nghĩa tất cả các object dữ liệu (ví dụ: Promotion, User, ErrorResponse) trong mục components/schemas và sử dụng $ref để tham chiếu đến chúng.
  • BẮT BUỘC Mọi trường dữ liệu dạng thời gian (timestamp) phải sử dụng định dạng ISO 8601 và được định nghĩa trong schema là type: string, format: date-time.
  • Tuân thủ quy tắc đặt tên: PascalCase cho tên schema (ví dụ: PromotionData), camelCase cho các thuộc tính (ví dụ: promotionTitle).

6. Đặc tả Responses Toàn diện:

  • Cấu trúc Response Thành công Chuẩn:

    • Mọi response body (cả thành công và thất bại) BẮT BUỘC phải có trường status ở cấp cao nhất ("success" hoặc "error").
    • SuccessResponse sẽ có cấu trúc { "status": "success", "data": {...}, "meta": {...} }.
    • ErrorResponse sẽ có cấu trúc { "status": "error", "code": "...", "message": "..." }.
    • meta BẮT BUỘC phải chứa requestId (UUID) và timestamp (ISO 8601).
    • Đối với các response trả về danh sách, meta BẮT BUỘC phải chứa object pagination bao gồm total, limit, và offset.
    {
      "status": "success",
      "data": { ... } | [ ... ],
      "meta": {
        "requestId": "uuid",
        "traceId": "uuid",
        "timestamp": "iso-8601-timestamp",
        "pagination": { ... } // (Tùy chọn, chỉ cho list)
      }
    }
    
  • Mô hình Lỗi Chuẩn:

    • ErrorResponse schema bao gồm code, message, và details (tùy chọn).
    • Mã lỗi (code) BẮT BUỘC phải theo format PREFIX_SUFFIX, với các PREFIX chuẩn hóa (ví dụ: AUTH_, PROMOTION_, SYS_).
    {
      "status": "error",
      "httpStatus": 400,
      "code": "VALIDATION_ERROR",
      "message": "Invalid input parameters.",
      "details": [
        {
          "field": "title",
          "issue": "must not be empty"
        }
      ]
    }
    
  • Mỗi endpoint phải định nghĩa tất cả các response code có thể xảy ra, không chỉ là 200 OK. Tối thiểu phải bao gồm:

    • 2xx (ví dụ: 200 OK, 201 Created, 204 No Content)
    • 400 Bad Request (khi request từ client bị sai)
    • 401 Unauthorized (khi chưa xác thực)
    • 403 Forbidden (khi không có quyền)
    • 404 Not Found (khi không tìm thấy tài nguyên)
    • 500 Internal Server Error (cho các lỗi phía server)
  • Nên định nghĩa các response lỗi phổ biến trong components/responses để tái sử dụng.
Prefix Ý nghĩa Ví dụ
AUTH_ Lỗi liên quan đến Xác thực & Phân quyền AUTH_TOKEN_EXPIRED
PROMOTION_ Lỗi liên quan đến resource Khuyến mãi PROMOTION_NOT_FOUND
FAQ_ Lỗi liên quan đến resource FAQ FAQ_INVALID_ID
VALIDATION_ Lỗi liên quan đến xác thực dữ liệu đầu vào VALIDATION_ERROR
SYS_ Lỗi hệ thống chung SYS_INTERNAL_ERROR

7. Quy chuẩn Query Parameters:

  • Đối với các endpoint trả về danh sách, BẮT BUỘC phải hỗ trợ các query parameters sau để phân trang: limit (số lượng record mỗi trang) và offset (vị trí bắt đầu lấy).
  • Sắp xếp (sort): Các endpoint trả về danh sách nên hỗ trợ sort theo format sort=fieldName:direction (ví dụ: sort=createdAt:desc).
  • Lọc (filter): Nên hỗ trợ lọc theo format filter[fieldName]=value (ví dụ: filter[status]=active).

8. Quy chuẩn HTTP Headers:

X-Request-Id header MUST match meta.requestId X-Trace-Id header MUST match meta.traceId.

  • Mọi response từ server BẮT BUỘC phải chứa header X-Request-Id. Giá trị của header này phải giống hệt với trường requestId trong meta của response body.
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix timestamp).

9. Bảo mật (securitySchemes):

  • Cơ chế xác thực (ví dụ: BearerAuth) phải được định nghĩa rõ ràng trong components/securitySchemes.
  • Mọi endpoint yêu cầu xác thực BẮT BUỘC phải có mục security tham chiếu đến cơ chế này.

10. Cung cấp Ví dụ (example):

  • Khuyến khích cung cấp các giá trị ví dụ cho cả request và response để giúp đội ngũ frontend dễ dàng hiểu và tích hợp.

Template OpenAPI Mẫu

Và để bắt đầu, đây là một template .yaml mà chúng ta có thể sử dụng cho cả 3 file API. Nó đã bao gồm sẵn các cấu trúc và quy chuẩn ở trên.

openapi: 3.0.3
info:
  title: "EZD AI Booth - [Tên Service] API"
  description: "Tuân thủ Hiến pháp API."
  version: "1.0.0"
  contact: { name: "EZDesign Tech Team", email: "tech@ezdesign.vn" }

servers:
  - url: "https://api.ezdesign.vn/v1"
    description: "Production Server"
  - url: "https://api.staging.ezdesign.vn/v1"
    description: "Staging Server"

tags:
  - name: "Promotions"
    description: "Quản  các chương trình khuyến mãi"

paths:
  /promotions:
    get:
      tags: ["Promotions"]
      summary: "Lấy danh sách khuyến mãi"
      operationId: "listPromotions"
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Sort'
        - $ref: '#/components/parameters/FilterStatus'
      responses:
        '200': { $ref: '#/components/responses/PromotionListSuccess' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }        
        '500': { $ref: '#/components/responses/InternalServerError' }
    post:
      tags: ["Promotions"]
      summary: "Tạo một khuyến mãi mới"
      operationId: "createPromotion"
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/PromotionCreate'
      responses:
        '201': { $ref: '#/components/responses/PromotionSuccess' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /promotions/{id}:
    parameters:
      - $ref: '#/components/parameters/PromotionId'

    get:
      summary: "Lấy chi tiết khuyến mãi"
      operationId: "getPromotion"
      responses:
        '200': 
          $ref: '#/components/responses/PromotionSuccess'
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '412': { $ref: '#/components/responses/PreconditionFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }        
        '500': { $ref: '#/components/responses/InternalServerError' }
    patch:
      summary: "Cập nhật khuyến mãi"
      operationId: "updatePromotion"
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PromotionUpdateRequest' }
            example:
              title: "Cyber Monday Sale"
              status: "active"
      responses:
        '200': { $ref: '#/components/responses/PromotionSuccess' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '412': { $ref: '#/components/responses/PreconditionFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }        
        '500': { $ref: '#/components/responses/InternalServerError' }

    delete:
      summary: "Xóa khuyến mãi"
      operationId: "deletePromotion"
      responses:
        '204':
          description: "Xóa thành công, không  nội dung trả về"
          headers:
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
            X-Trace-Id: { $ref: '#/components/headers/TraceId' }
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
            # ETag: { $ref: '#/components/headers/ETag' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '412': { $ref: '#/components/responses/PreconditionFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalServerError' }

components:
  schemas:
    # CẬP NHẬT: Hoàn thiện ErrorResponse với meta
    ErrorResponse:
      type: object
      required: [status, httpStatus, code, message, meta]
      properties:
        status: { type: string, enum: [error] }
        httpStatus: { type: integer, minimum: 400, maximum: 599 }
        code: { type: string, description: "Format: PREFIX_SUFFIX" }
        message: { type: string, maxLength: 250 }
        details:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              issue: { type: string }
        meta:
          $ref: '#/components/schemas/MetaBase'

    MetaBase:
      type: object
      required: [requestId, traceId, timestamp]
      properties:
        requestId: { type: string, format: uuid }
        traceId: { type: string, format: uuid, description: "ID để trace request qua nhiều service." }
        timestamp: { type: string, format: date-time }

    PromotionCreateRequest:
      type: object
      required: [title, description, startDate, endDate]
      properties:
        title: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 500 }
        startDate: { type: string, format: date-time }
        endDate: { type: string, format: date-time }
        imageUrl: { type: string, format: uri }
        status: { type: string, enum: [active, scheduled], default: "scheduled" }

    PromotionUpdateRequest:
      type: object
      properties:
        title: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 500 }
        startDate: { type: string, format: date-time }
        endDate: { type: string, format: date-time }
        imageUrl: { type: string, format: uri }
        status: { type: string, enum: [active, expired, scheduled] }

    SuccessResponseBase:
      type: object
      required: [status, meta]
      properties:
        status: { type: string, enum: [success] }
        meta: { $ref: '#/components/schemas/MetaBase' }

    PromotionResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessResponseBase'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/Promotion' }

    MetaWithPagination:
      allOf:
        - $ref: '#/components/schemas/MetaBase'
        - type: object
          properties:
            pagination: { $ref: '#/components/schemas/Pagination' }

    PromotionListResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessResponseBase'
        - type: object
          properties:
            data:
              type: array
              items: { $ref: '#/components/schemas/Promotion' }
            meta:
              $ref: '#/components/schemas/MetaWithPagination'

    Pagination:
      type: object
      required: [total, limit, offset]
      properties:
        total: { type: integer, description: "Tổng số lượng record." }
        limit: { type: integer, description: "Số lượng record trên mỗi trang." }
        offset: { type: integer, description: "Vị trí bắt đầu của trang." }

    Promotion:
      type: object
      required: [id, title, status, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid, readOnly: true }
        title: { type: string, minLength: 1, maxLength: 120 }
        status: { type: string, enum: [active, expired, scheduled] }
        createdAt: { type: string, format: date-time, readOnly: true }
        updatedAt: { type: string, format: date-time, readOnly: true }

  parameters:
    TenantId:
      name: X-Tenant-Id
      in: header
      required: true
      description: "UUID của tenant. MUST thuộc danh sách tenant trong JWT."
      schema: { type: string, format: uuid }

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: "UUID để chống tạo trùng khi retry; server giữ kết quả trong ~24h."
      schema: { type: string, format: uuid }

    IfMatch:
      name: If-Match
      in: header
      description: "ETag của resource để thực hiện optimistic locking."
      schema: { type: string }

    Limit:
      name: limit
      in: query
      schema: { type: integer, default: 20, minimum: 1, maximum: 100 }

    Offset:
      name: offset
      in: query
      schema: { type: integer, default: 0, minimum: 0 }

    Sort:
      name: sort
      in: query
      description: "Format: field:direction. field  {createdAt,updatedAt,title,status}"
      schema:
        type: string
        pattern: "^(createdAt|updatedAt|title|status):(asc|desc)$"
        example: "createdAt:desc"

    FilterStatus: 
      name: "filter[status]"
      in: query
      schema: 
        type: string
        enum: [active, expired, scheduled]

    PromotionId:
      name: id
      in: path
      required: true
      description: "UUID của khuyến mãi"
      schema:
        type: string
        format: uuid

  headers:
    X-Tenant-Id:
      description: "Echo lại tenant của request." #Quy tắc: mọi endpoint MUST yêu cầu X-Tenant-Id (booth & cms).Response NÊN echo header X-Tenant-Id để dễ trace/log.
      schema: { type: string, format: uuid }

    X-RateLimit-Limit: { schema: { type: integer } }
    X-RateLimit-Remaining: { schema: { type: integer } }
    X-RateLimit-Reset: { description: "Unix epoch", schema: { type: integer } }
    Location:
      description: "URL của tài nguyên vừa tạo"
      schema: { type: string, format: uri }
    ETag:
      description: "Định danh phiên bản của resource."
      schema: { type: string }
    RequestId: { description: "UUID của request. X-Request-Id header MUST match meta.requestId", schema: { type: string, format: uuid } }
    TraceId: { description: "UUID để trace request qua nhiều service. X-Trace-Id header MUST match meta.traceId", schema: { type: string, format: uuid } }
    Retry-After:
      description: "Số giây hoặc HTTP-date khi  thể thử lại."
      schema: { type: string }

  requestBodies:
    PromotionCreate:
      required: true
      content:
        application/json:
          schema: { $ref: '#/components/schemas/PromotionCreateRequest' }
          example:
            title: "Black Friday Sale"
            description: "Giảm giá toàn bộ sản phẩm 50%"
            startDate: "2025-11-25T00:00:00Z"
            endDate: "2025-11-30T23:59:59Z"
            imageUrl: "https://cdn.ezdesign.vn/promo/bf.jpg"
            status: "scheduled"

    PromotionUpdate:
      required: true
      content:
        application/json:
          schema: { $ref: '#/components/schemas/PromotionUpdateRequest' }
          example:
            title: "Cyber Monday Sale"
            status: "active"            

  responses:
    TooManyRequests:
      description: "Quá hạn mức gọi API"
      headers:
        Retry-After: { $ref: '#/components/headers/Retry-After' }
        X-Request-Id: { $ref: '#/components/headers/RequestId' }
        X-Trace-Id: { $ref: '#/components/headers/TraceId' }
        X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example:
            status: "error"
            httpStatus: 429
            code: "SYS_RATE_LIMITED"
            message: "Too many requests."
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }

    PreconditionFailed:
      description: "ETag không khớp (If-Match)"
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example:
            status: "error"
            httpStatus: 412
            code: "VALIDATION_PRECONDITION_FAILED"
            message: "ETag does not match."
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }

    PromotionSuccess:
      description: "Thao tác thành công"
      headers:
        Location: { $ref: '#/components/headers/Location' }
        ETag: { $ref: '#/components/headers/ETag' } #current resource version
        X-Request-Id: { $ref: '#/components/headers/RequestId' }
        X-Trace-Id: { $ref: '#/components/headers/TraceId' }
        X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/PromotionResponse' }
          example:
            status: "success"
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }
            data: { id: "uuid", title: "...", status: "scheduled", createdAt: "iso-date", updatedAt: "iso-date" }
    PromotionListSuccess:
      description: "Lấy danh sách thành công"
      headers:
        X-Request-Id: { $ref: '#/components/headers/RequestId' }
        X-Trace-Id: { $ref: '#/components/headers/TraceId' }
        X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/PromotionListResponse' }
          example:
            status: "success"
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date", pagination: { total: 1, limit: 20, offset: 0 } }
            data: [{ id: "uuid", title: "...", status: "active", createdAt: "iso-date", updatedAt: "iso-date" }]

    BadRequest:
      description: "Tham số yêu cầu không hợp lệ"
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example:
            status: "error"
            httpStatus: 400
            code: "VALIDATION_ERROR"
            message: "Invalid parameter(s)."
            details: [{ field: "limit", issue: "must be <= 100" }]
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }

    Forbidden:
      description: "Không  quyền thực hiện hành động này"
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example:
            status: "error"
            httpStatus: 403
            code: "AUTH_FORBIDDEN"
            message: "Permission denied."
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }

    Unauthorized:
      description: "Chưa xác thực hoặc token không hợp lệ"
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example:
            status: "error"
            httpStatus: 401
            code: "AUTH_UNAUTHORIZED"
            message: "Missing or invalid authentication token."
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }

    InternalServerError:
      description: "Lỗi hệ thống không mong muốn"
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example:
            status: "error"
            httpStatus: 500
            code: "SYS_INTERNAL_ERROR"
            message: "An unexpected internal server error occurred."
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }

    NotFound:
      description: "Tài nguyên không tồn tại"
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example:
            status: "error"
            httpStatus: 404
            code: "PROMOTION_NOT_FOUND"
            message: "Promotion not found."
            meta: { requestId: "uuid", traceId: "uuid", timestamp: "iso-date" }

  securitySchemes:
    BearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }

security:
  - BearerAuth: []