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àversionrõ ràng. contactphả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:
productionvàstaging(hoặcdevelopment).
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ộttagcó ý 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
requestBodyhoặcresponses. - 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ụccomponents/schemasvà 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 8601và được định nghĩa trong schema làtype: string, format: date-time. - Tuân thủ quy tắc đặt tên:
PascalCasecho tên schema (ví dụ:PromotionData),camelCasecho 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"). SuccessResponsesẽ có cấu trúc{ "status": "success", "data": {...}, "meta": {...} }.ErrorResponsesẽ có cấu trúc{ "status": "error", "code": "...", "message": "..." }.metaBẮT BUỘC phải chứarequestId(UUID) vàtimestamp(ISO 8601).- Đối với các response trả về danh sách,
metaBẮT BUỘC phải chứa objectpaginationbao gồmtotal,limit, vàoffset.
{ "status": "success", "data": { ... } | [ ... ], "meta": { "requestId": "uuid", "traceId": "uuid", "timestamp": "iso-8601-timestamp", "pagination": { ... } // (Tùy chọn, chỉ cho list) } } - Mọi response body (cả thành công và thất bại) BẮT BUỘC phải có trường
-
Mô hình Lỗi Chuẩn:
ErrorResponseschema bao gồmcode,message, vàdetails(tùy chọn).- Mã lỗi (
code) BẮT BUỘC phải theo formatPREFIX_SUFFIX, với cácPREFIXchuẩ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ợsorttheo formatsort=fieldName:direction(ví dụ:sort=createdAt:desc). - Lọc (
filter): Nên hỗ trợ lọc theo formatfilter[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ườngrequestIdtrongmetacủ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 trongcomponents/securitySchemes. - Mọi endpoint yêu cầu xác thực BẮT BUỘC phải có mục
securitytham 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 lý 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 có 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 có 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 có 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: []