Danh sách bài viết

Bài 8: OpenAPI & Swagger — API Documentation

Bài 8 của series Rust RESTful API — phân biệt OpenAPI Specification (spec định dạng) và Swagger (tooling kế thừa) thường bị nhầm lẫn; mổ xẻ cấu trúc OpenAPI 3.1 với các root key chính (info, servers, paths, components.schemas, components.securitySchemes, security, tags) và đoạn YAML mẫu cho endpoint GET /api/v1/products/:slug; giới thiệu ba UI tool render spec phổ biến nhất (Swagger UI với "Try it out", ReDoc 3-pane clean, Scalar modern); hướng dẫn crate utoipa cho axum với derive macro ToSchema, attribute #[utoipa::path] và struct #[derive(OpenApi)] để auto-generate spec từ code; so sánh workflow code-first (Rust dev phổ biến) vs API-first (enterprise) và lý do Shop API chọn code-first qua utoipa; mount endpoint /api-doc/openapi.json + /swagger-ui ở dev/staging và tắt production để tránh leak schema; tích hợp BearerAuth security scheme qua trait Modify cho JWT.

12/06/2026
10 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Phân biệt rõ OpenAPI (spec định dạng) và Swagger (tooling kế thừa) — hai term hay bị dùng lẫn lộn.
  • Nắm cấu trúc OpenAPI 3.1 với các root key chính: info, servers, paths, components (schemas + securitySchemes), security, tags.
  • Biết ba UI tool render spec phổ biến: Swagger UI (cổ điển, có "Try it out"), ReDoc (3-pane clean cho document hóa), Scalar (modern, API client built-in).
  • Hiểu crate utoipa cho axum với derive macro ToSchema, attribute #[utoipa::path], struct #[derive(OpenApi)] auto-generate spec từ code.
  • Hiểu trade-off code-first (code = source of truth) vs API-first (design-by-contract).
  • Áp dụng vào Shop API: expose /api-doc/openapi.json + /swagger-ui ở dev/staging, tắt production; tích hợp BearerAuth security scheme qua trait Modify.
2

OpenAPI vs Swagger — Đừng Confused

Hai term OpenAPISwagger thường bị dùng thay thế nhau, nhưng kỹ thuật là hai khái niệm tách bạch. Phân biệt rõ ngay từ đầu giúp bạn đọc tài liệu, blog, job description không bị nhầm.

Swagger ra đời năm 2010 do Tony Tam tại Reverb Technologies, là toolkit gốc để thiết kế và document REST API. Ba sản phẩm chính của Swagger thời đó: Swagger Specification (định dạng JSON/YAML mô tả API), Swagger UI (web app render spec), Swagger Codegen (sinh client/server code từ spec). SmartBear mua lại Swagger năm 2015.

Tháng 1 năm 2016, SmartBear donate Swagger Specification cho Linux Foundation, đổi tên thành OpenAPI Specification (OAS) và thành lập OpenAPI Initiative với thành viên sáng lập Google, IBM, Microsoft, PayPal, ... Spec chính thức rebrand: Swagger 2.0 trở thành nền tảng cho OpenAPI 3.0 (2017), tiếp đến OpenAPI 3.1 (2021) là phiên bản hiện hành tại thời điểm bài viết.

Từ 2016 trở đi, hai term phân nhánh rõ ràng:

  • OpenAPI = spec định dạng, là chuẩn mô tả REST API (root keys, schema, security, ...). OpenAPI 3.1 được fully align với JSON Schema Draft 2020-12 — đây là điểm quan trọng vì crate Rust schemars sinh JSON Schema 2020-12 ra có thể nhúng thẳng vào spec OpenAPI 3.1.
  • Swagger = tên brand của tooling do SmartBear maintain. Sản phẩm còn dùng phổ biến: Swagger UI (render spec), Swagger Editor (IDE web edit YAML), Swagger Codegen / OpenAPI Generator (codegen client SDK 50+ ngôn ngữ).

Tóm gọn: OpenAPI là spec, Swagger UI là một trong nhiều tool render spec đó. Khi đồng nghiệp nói "viết Swagger" thường ý là viết spec OpenAPI; "deploy Swagger" ý là deploy Swagger UI. Series này dùng đúng tên: OpenAPI cho spec, Swagger UI cho UI tool.

3

OpenAPI 3.1 Cấu Trúc Chính

Một file OpenAPI 3.1 là document JSON hoặc YAML có root keys cố định:

  • openapi — version spec, vd "3.1.0".
  • info — metadata API: title, version, description, contact, license.
  • servers — danh sách base URL theo môi trường (dev, staging, prod).
  • paths — định nghĩa endpoint; mỗi path có HTTP method (get, post, ...) với operationId, parameters, requestBody, responses, tags, security.
  • components — section reusable: schemas (DTO definitions tham chiếu qua $ref), securitySchemes (auth scheme), parameters, responses, examples.
  • security — apply security scheme ở scope global cho cả API.
  • tags — gom nhóm endpoint trong UI (vd "Auth", "Catalog", "Cart").

Đoạn YAML mẫu cho endpoint GET /api/v1/products/:slug của Shop API:

openapi: 3.1.0
info:
  title: Shop API
  version: 0.1.0
  description: E-commerce REST API
  contact:
    name: Shop API Team
    email: [email protected]
  license:
    name: MIT

servers:
  - url: http://localhost:3000
    description: Development
  - url: https://staging.api.shop.example
    description: Staging

paths:
  /api/v1/products/{slug}:
    get:
      tags: [Catalog]
      operationId: getProductBySlug
      summary: Get product detail by slug
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Product found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  schemas:
    Product:
      type: object
      required: [id, slug, name, price]
      properties:
        id:
          type: integer
          format: int64
        slug:
          type: string
        name:
          type: string
        price:
          type: string
          description: Decimal as string
    ErrorResponse:
      type: object
      required: [error, code, request_id]
      properties:
        error: { type: string }
        code: { type: string }
        request_id: { type: string }
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

Reference reuse qua $ref: '#/components/schemas/Product' tránh lặp; ProductErrorResponse được dùng lại ở nhiều path khác nhau. Đây là pattern chuẩn — không viết schema inline cho mỗi path response.

4

Swagger UI / ReDoc / Scalar — Ba UI Tool

Spec OpenAPI là JSON/YAML không có giá trị trực tiếp với end-user (developer consume API). Phải render qua một UI tool. Ba lựa chọn phổ biến nhất tại thời điểm 2026:

Swagger UI — UI gốc do SmartBear maintain, mature nhất, được dùng rộng nhất. Render đầy đủ schema, response example, có nút "Try it out" trên mỗi endpoint cho phép developer gọi API thật ngay trong browser (điền tham số, click Execute, xem response). Phân phối qua npm package swagger-ui-dist hoặc CDN. Trải nghiệm cổ điển nhưng quen thuộc với mọi backend dev.

ReDoc — UI focus vào document hóa hơn là interactivity. Layout 3-pane (sidebar navigation, nội dung chính, code samples), kiểu document tham khảo hơn playground. KHÔNG có "Try it out" mặc định (có plugin Try it nhưng không tích hợp sẵn). Phù hợp public API document trên website marketing dạng docs.api.example nơi clean look quan trọng hơn tính tương tác. Stripe và GitHub dùng phong cách tương tự ReDoc.

Scalar — UI modern (xuất hiện 2023, phổ biến từ 2024), fast load, dark mode mặc định, có API client built-in (mạnh hơn "Try it out" của Swagger UI, có thể save request collection như Postman lite). Crate Rust hỗ trợ qua utoipa-scalar. Thích hợp project mới muốn UX hiện đại không bị "cổ" như Swagger UI.

Cả ba đều consume cùng file openapi.json. Cấu hình chỉ là trỏ UI tới URL spec: spec.url = "/api-doc/openapi.json". Có thể mount nhiều UI cùng lúc trên cùng project — vd /swagger-ui cho dev test, /redoc cho document public.

Shop API chọn Swagger UI mặc định cho dev/staging vì có "Try it out" giúp dev test endpoint nhanh không cần mở Postman. Nếu sau này cần document public cho third-party developer, sẽ thêm ReDoc song song; production runtime tắt cả hai (chi tiết section 7).

5

utoipa Crate — Auto-Generate OpenAPI Từ axum

utoipa là crate Rust de facto cho code-first OpenAPI generation, dùng derive macro để sinh spec từ struct DTO và axum handler. Ecosystem có vài crate phối hợp:

  • utoipa (core) — derive macro ToSchema, OpenApi, attribute #[utoipa::path].
  • utoipa-swagger-ui — embed Swagger UI assets vào binary và serve qua axum router.
  • utoipa-axum — tích hợp typed router với utoipa (collect path tự động qua macro routes!).
  • utoipa-scalar / utoipa-redoc — alternative UI cho ai muốn Scalar hoặc ReDoc.

Phiên bản lock cho Shop API: utoipa 5, utoipa-swagger-ui 8, utoipa-axum 0.1. Cargo.toml preview:

# Cargo.toml — shop-api crate (preview cho B12-G6)
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

# OpenAPI documentation
utoipa = { version = "5", features = ["axum_extras", "chrono", "decimal"] }
utoipa-swagger-ui = { version = "8", features = ["axum"] }
utoipa-axum = "0.1"

Pattern code-first qua utoipa có ba thành phần. Thứ nhất, derive ToSchema cho mọi DTO để utoipa sinh schema entry vào components.schemas:

// File: crates/shop-api/src/handlers/products.rs
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct Product {
    /// Product ID (BIGSERIAL)
    pub id: i64,
    /// URL-friendly slug
    pub slug: String,
    pub name: String,
    /// Price as decimal string (vd "1499.00")
    pub price: String,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct ErrorResponse {
    pub error: String,
    pub code: String,
    pub request_id: String,
}

Thứ hai, attribute #[utoipa::path] trên handler axum mô tả method, path, parameters, responses:

use axum::{extract::Path, Json};
use axum::http::StatusCode;

#[utoipa::path(
    get,
    path = "/api/v1/products/{slug}",
    tag = "Catalog",
    params(
        ("slug" = String, Path, description = "Product slug")
    ),
    responses(
        (status = 200, description = "Product found", body = Product),
        (status = 404, description = "Product not found", body = ErrorResponse)
    )
)]
pub async fn get_product_by_slug(
    Path(slug): Path,
) -> Result, (StatusCode, Json)> {
    // Implementation chi tiết ở G7 CRUD
    todo!("read from PostgreSQL via ProductRepo")
}

Thứ ba, struct ApiDoc với #[derive(OpenApi)] gom tất cả path và schema lại thành một spec duy nhất:

// File: crates/shop-api/src/openapi.rs
use utoipa::OpenApi;
use crate::handlers::products::{
    get_product_by_slug, Product, ErrorResponse,
};

#[derive(OpenApi)]
#[openapi(
    info(
        title = "Shop API",
        version = "0.1.0",
        description = "E-commerce REST API"
    ),
    servers(
        (url = "http://localhost:3000", description = "Development"),
        (url = "https://staging.api.shop.example", description = "Staging")
    ),
    paths(get_product_by_slug),
    components(schemas(Product, ErrorResponse)),
    tags(
        (name = "Catalog", description = "Product catalog endpoints")
    )
)]
pub struct ApiDoc;

Gọi ApiDoc::openapi() trả về object utoipa::openapi::OpenApi, serialize ra JSON/YAML qua serde tự động. Mỗi khi thêm handler mới hoặc DTO mới, chỉ cần append vào paths(...)components(schemas(...)) — spec đồng bộ với code tại compile time.

6

Code-First vs API-First Workflow

Hai workflow chính trong industry để duy trì sync giữa speccode:

Code-first (phổ biến trong Rust dev và startup):

  • Viết Rust handler và DTO struct trước.
  • Derive macro (utoipa ToSchema, #[utoipa::path], #[derive(OpenApi)]) sinh OpenAPI spec từ code.
  • Pros: code là single source of truth, không bao giờ drift; refactor handler tự động propagate sang spec; CI dễ check (spec là output của cargo build); learning curve thấp cho Rust dev.
  • Cons: spec phụ thuộc implementation, không có giai đoạn "design contract" trước khi code; khó cho frontend/mobile làm song song nếu Rust dev chưa code xong; backend dev có thể accidentally đổi schema breaking client mà không nhận ra.

API-first (phổ biến trong enterprise và team đa nền tảng):

  • Viết OpenAPI YAML/JSON trước trong giai đoạn design (vd dùng Swagger Editor, Stoplight Studio, Apicurio).
  • Codegen Rust server skeleton từ spec (tool: oapi-codegen, openapi-generator-cli), backend dev fill in business logic.
  • Codegen client SDK cho frontend/mobile cùng từ spec, làm song song.
  • Pros: contract clear ngay từ đầu, stakeholder review/approve trước khi code; frontend/mobile không bị block chờ backend; spec là nguồn quyền lực, breaking change phải qua review formal.
  • Cons: code có thể drift khỏi spec nếu dev sửa Rust mà quên update YAML; cần CI gate check sync (oapi-codegen diff hoặc test integration); maintenance cost cao hơn cho solo dev / small team.

Shop API lock code-first qua utoipa (đã quyết định ở B6 JSON Format Policy). Lý do phù hợp series:

  • Dạy theo step-by-step incremental, viết code trước rồi document sau là natural flow.
  • Mỗi bài thêm handler mới, derive macro tự động cập nhật spec — không phải maintain hai source.
  • Solo dev / small team không cần ceremony design-by-contract của API-first.
  • Đến khi cần API-first (vd Shop có frontend team làm song song), có thể export spec từ code-first ra YAML và dùng làm contract baseline — đảo workflow không bị mất công.

Nếu bạn vào project enterprise cần API-first, tool oapi-codegen (Rust port của openapi-codegen) sinh axum handler skeleton từ YAML; hoặc openapi-generator-cli hỗ trợ Rust template với rust-axum generator.

7

Endpoint Expose: /api-doc/openapi.json + /swagger-ui

Shop API expose hai endpoint cho documentation:

  • GET /api-doc/openapi.json — trả spec JSON đầy đủ. Tool externals (Postman import, codegen, monitoring) consume URL này.
  • GET /swagger-ui — UI Swagger render spec, dev/QA dùng test endpoint qua "Try it out".

Cấu hình axum trong main.rs:

// File: crates/shop-api/src/main.rs
use axum::{Router, routing::get, Json};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::openapi::ApiDoc;
use crate::handlers::products::get_product_by_slug;

#[tokio::main]
async fn main() -> Result<(), Box> {
    let config = AppConfig::from_env()?;

    let api_router = Router::new()
        .route("/products/{slug}", get(get_product_by_slug));

    let mut app = Router::new()
        .nest("/api/v1", api_router);

    // Mount documentation chỉ ở dev/staging
    if config.env != "production" {
        app = app
            .route(
                "/api-doc/openapi.json",
                get(|| async { Json(ApiDoc::openapi()) }),
            )
            .merge(
                SwaggerUi::new("/swagger-ui")
                    .url("/api-doc/openapi.json", ApiDoc::openapi()),
            );
    }

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

Logic if config.env != "production" đảm bảo runtime production hoàn toàn không có route documentation — request GET /swagger-ui trả 404 thay vì render UI. Lý do tắt production:

  • Tránh leak schema chi tiết ra public — đối thủ và attacker có thể đọc endpoint nội bộ, parameter, response shape để map surface attack.
  • Giảm attack surface — Swagger UI có lịch sử CVE liên quan XSS, prototype pollution; mỗi endpoint extra là một bề mặt tiềm năng.
  • Giảm binary size nhỏ — không ship Swagger UI assets cho production runtime (utoipa-swagger-ui embed ~2MB JS/CSS).

Internal team vẫn cần spec để consume API ở production (vd partner integration). Solution: artifact CI build sinh file openapi.json ra GitHub Release hoặc internal artifact registry; team frontend/mobile/partner pull file đó về dùng làm contract. Không expose qua runtime endpoint.

// File: crates/shop-api/src/bin/export_openapi.rs
// Binary phụ chạy ở CI để export spec
use shop_api::openapi::ApiDoc;
use utoipa::OpenApi;

fn main() -> Result<(), Box> {
    let spec = ApiDoc::openapi();
    let json = serde_json::to_string_pretty(&spec)?;
    std::fs::write("openapi.json", json)?;
    println!("OpenAPI spec exported to openapi.json");
    Ok(())
}

GitHub Actions chạy cargo run --bin export_openapi mỗi PR, upload file làm artifact, tag release attach file → internal team có spec mới nhất không cần production endpoint.

8

Tích Hợp Với Security: Bearer JWT

OpenAPI 3.1 hỗ trợ nhiều security scheme: HTTP basic, HTTP bearer, API key (header/query/cookie), OAuth2 (4 flow), OpenID Connect. Shop API lock BearerAuth qua JWT (đã quyết định ở B4 header convention). Schema scheme:

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

Utoipa support thêm security scheme qua trait Modify — implement trait này cho struct riêng (vd SecurityAddon), gọi trong attribute #[openapi(modifiers(&SecurityAddon))]:

// File: crates/shop-api/src/openapi.rs (mở rộng)
use utoipa::{
    openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
    Modify, OpenApi,
};

pub struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        let components = openapi
            .components
            .as_mut()
            .expect("components register first");
        components.add_security_scheme(
            "BearerAuth",
            SecurityScheme::Http(
                HttpBuilder::new()
                    .scheme(HttpAuthScheme::Bearer)
                    .bearer_format("JWT")
                    .build(),
            ),
        );
    }
}

#[derive(OpenApi)]
#[openapi(
    modifiers(&SecurityAddon),
    info(title = "Shop API", version = "0.1.0"),
    paths(get_product_by_slug, get_me),
    components(schemas(Product, ErrorResponse, User)),
)]
pub struct ApiDoc;

Apply security per handler bằng attribute security(...) trong #[utoipa::path]. Endpoint cần auth liệt kê scheme; endpoint public bỏ trống:

#[utoipa::path(
    get,
    path = "/api/v1/me",
    tag = "User",
    security(("BearerAuth" = [])),  // require Bearer JWT
    responses(
        (status = 200, description = "Current user", body = User),
        (status = 401, description = "Unauthenticated", body = ErrorResponse)
    )
)]
pub async fn get_me(/* CurrentUser extractor */) -> Json {
    todo!()
}

#[utoipa::path(
    get,
    path = "/api/v1/products/{slug}",
    tag = "Catalog",
    // KHÔNG có security — endpoint public
    responses(/* ... */)
)]
pub async fn get_product_by_slug(/* ... */) {
    todo!()
}

Trên Swagger UI, sau khi spec có securitySchemes.BearerAuth, hiện nút Authorize ở góc phải. Click → paste JWT token → click Authorize. Mọi "Try it out" trên endpoint có security(("BearerAuth" = [])) sẽ tự động đính kèm Authorization: Bearer <token>; endpoint public không có security thì không cần token. Đây là workflow chuẩn dev test endpoint cần auth.

Chi tiết JWT signing, claim, refresh token sẽ phân tích sâu ở Group 12 (B111-B120). Tại B8 bài này chỉ lock spec scheme cho documentation: BearerAuth { type: http, scheme: bearer, bearerFormat: JWT } — naming này dùng xuyên suốt series, mọi handler cần auth reference cùng key "BearerAuth".

9

Tổng Kết

  • OpenAPI 3.1 = spec định dạng (do OpenAPI Initiative maintain từ 2016); Swagger = tên brand tooling (Swagger UI, Swagger Editor, Swagger Codegen do SmartBear maintain). Không dùng lẫn hai term.
  • Cấu trúc OpenAPI: root keys info, servers, paths, components.schemas (DTO reusable qua $ref), components.securitySchemes, security, tags. OpenAPI 3.1 fully align với JSON Schema 2020-12.
  • Ba UI tool render spec: Swagger UI (cổ điển, "Try it out"), ReDoc (3-pane clean cho document hóa), Scalar (modern 2024+, API client built-in). Cùng consume một file openapi.json.
  • utoipa crate: code-first OpenAPI cho axum qua derive macro. Pattern ba thành phần — #[derive(ToSchema)] cho DTO, #[utoipa::path(...)] trên handler, #[derive(OpenApi)] cho struct ApiDoc gom path + schema.
  • Code-first vs API-first: code-first dùng utoipa derive (code = source of truth, không drift, learning curve thấp); API-first viết YAML trước rồi codegen (contract clear, frontend/mobile làm song song, cần CI gate sync). Shop API chọn code-first vì solo dev / small team.
  • Endpoint expose: GET /api-doc/openapi.json + GET /swagger-ui ở dev/staging; production tắt cả hai để tránh leak schema và giảm attack surface. Internal team consume spec qua artifact CI build (file openapi.json export ra GitHub Release).
  • BearerAuth security scheme: inject qua trait Modify (struct SecurityAddon trong openapi.rs), apply per handler qua security(("BearerAuth" = [])). Swagger UI có nút "Authorize" cho dev paste token test endpoint.
  • Cargo.toml lock version: utoipa 5, utoipa-swagger-ui 8 (feature axum), utoipa-axum 0.1 — preview cho Group 6 trở đi khi handler thực tế xuất hiện.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. OpenAPI và Swagger có cùng nghĩa không? Phân biệt rõ hai term này về mặt lịch sử và phạm vi áp dụng hiện tại. Khi đồng nghiệp nói "viết Swagger" hoặc "deploy Swagger" thì thường ý là gì?
  2. Crate utoipa theo workflow code-first hay API-first? Shop API chọn workflow nào và lý do? Liệt kê ưu/nhược điểm code-first so với API-first cho team có frontend làm song song.
  3. Trong spec OpenAPI, section components.schemas dùng để làm gì? Cho ví dụ một schema Product được dùng lại ở nhiều path endpoint. Tại sao nên dùng $ref thay vì viết schema inline cho từng response?
  4. Tại sao Shop API tắt Swagger UI ở production environment? Nêu ba lý do cụ thể. Internal team vẫn cần spec để consume API ở production — giải pháp là gì để không expose endpoint runtime mà vẫn có spec cho team?
  5. Cấu trúc securitySchemes cho JWT Bearer trong OpenAPI viết như thế nào (3 field bắt buộc)? Trong utoipa, làm thế nào để inject security scheme vào spec? Apply scheme đó cho từng handler ra sao? Endpoint public không cần auth có khai báo security(...) không?
Đáp án
  1. KHÔNG cùng nghĩa. OpenAPIspec định dạng (chuẩn mô tả REST API qua JSON/YAML với root keys cố định), do OpenAPI Initiative dưới Linux Foundation maintain từ 2016 (rebrand từ Swagger Specification 2.0 do SmartBear donate). Phiên bản hiện hành OpenAPI 3.1 (2021). Swaggertên brand tooling do SmartBear sở hữu: Swagger UI (render spec), Swagger Editor (IDE web edit YAML), Swagger Codegen (sinh client SDK). Khi đồng nghiệp nói "viết Swagger" thường ý là viết spec OpenAPI (YAML/JSON); "deploy Swagger" thường ý là deploy Swagger UI để render spec. Series và Shop API dùng đúng tên: OpenAPI cho spec, Swagger UI cho UI tool — tránh nhầm lẫn khi nói chuyện về architecture.
  2. Utoipa theo code-first: viết Rust handler + DTO trước, derive macro (ToSchema, #[utoipa::path], #[derive(OpenApi)]) sinh OpenAPI spec từ code tại compile time. Shop API chọn code-first vì: (a) solo dev / small team không cần ceremony design-by-contract; (b) series dạy incremental, viết code trước rồi document sau là natural flow; (c) code = single source of truth, không bao giờ drift giữa spec và implementation. Ưu điểm code-first: không drift, refactor handler tự propagate sang spec, learning curve thấp cho Rust dev, CI dễ check. Nhược điểm so với API-first: không có giai đoạn "design contract" trước khi code → khó cho frontend/mobile làm song song nếu chưa có spec sớm; backend dev có thể accidentally đổi schema breaking client mà không nhận ra (cần CI test ngược lại). Nếu team có frontend/mobile làm song song và cần contract trước, API-first phù hợp hơn — viết YAML, codegen Rust skeleton qua oapi-codegen / openapi-generator-cli.
  3. Section components.schemas là kho định nghĩa reusable DTO, mỗi entry là một schema có tên (vd Product, User, ErrorResponse). Path endpoint reference qua $ref: '#/components/schemas/Product' thay vì viết schema inline. Ví dụ: schema Product dùng lại ở GET /api/v1/products/{slug} (response 200), GET /api/v1/products (response 200 với array Product), POST /api/v1/admin/products (response 201). Lợi ích $ref: (a) DRY — schema thay đổi chỉ sửa một chỗ ở components.schemas.Product, mọi path tự cập nhật; (b) UI tool render gom nhóm — Swagger UI hiển thị section "Schemas" tách riêng cho dev browse cấu trúc DTO; (c) Codegen client SDK sinh struct/class Product một lần dùng ở nhiều endpoint method; (d) File size nhỏ hơn với spec lớn (~60 endpoint Shop API, mỗi DTO inline 3-5 chỗ → tăng nhân ba file size).
  4. Shop API tắt Swagger UI ở production vì ba lý do: (1) Tránh leak schema chi tiết — Swagger UI expose toàn bộ endpoint, parameter, response shape, error code; đối thủ và attacker đọc spec map ra surface attack (vd thấy POST /api/v1/admin/products biết có admin endpoint, thấy error code: INVALID_TOKEN biết hệ thống có JWT). (2) Giảm attack surface — Swagger UI có lịch sử CVE liên quan XSS, prototype pollution, click-jacking; mỗi endpoint extra là bề mặt tiềm năng cho exploit. (3) Giảm binary size — không ship Swagger UI assets (~2MB JS/CSS embedded qua utoipa-swagger-ui) cho production runtime. Solution cho internal team vẫn có spec mà không expose endpoint: artifact CI build. Tạo binary phụ (vd src/bin/export_openapi.rs) gọi ApiDoc::openapi() serialize ra file openapi.json. GitHub Actions chạy binary này mỗi PR, upload file làm CI artifact, tag release attach file. Frontend/mobile/partner pull file openapi.json từ GitHub Release để codegen client SDK hoặc import Postman — không cần production endpoint.
  5. Cấu trúc securitySchemes cho JWT Bearer có ba field bắt buộc: type: http, scheme: bearer, bearerFormat: JWT. YAML đầy đủ: components.securitySchemes.BearerAuth = { type: http, scheme: bearer, bearerFormat: JWT }. Trong utoipa, inject security scheme qua trait Modify: implement impl Modify for SecurityAddon { fn modify(&self, openapi: &mut OpenApi) { ... } }, trong method modify gọi openapi.components.as_mut().unwrap().add_security_scheme("BearerAuth", SecurityScheme::Http(HttpBuilder::new().scheme(HttpAuthScheme::Bearer).bearer_format("JWT").build())), sau đó đăng ký vào ApiDoc qua attribute #[openapi(modifiers(&SecurityAddon), ...)]. Apply scheme cho từng handler qua attribute security(("BearerAuth" = [])) trong #[utoipa::path]. Mảng rỗng [] nghĩa không yêu cầu scope cụ thể (nếu dùng OAuth2 thì điền scope vào trong mảng vd ("OAuth2" = ["read:products", "write:products"])). Endpoint public không cần auth KHÔNG khai báo security(...) — utoipa hiểu là endpoint không yêu cầu security scheme. Trên Swagger UI, endpoint có security(...) hiện icon ổ khóa và "Try it out" auto-attach Authorization: Bearer <token> sau khi user click "Authorize" paste token; endpoint không có security không cần token để gọi.
11

Bài Tiếp Theo

— giới thiệu các tool CLI/GUI để test API nhanh: curl (universal, verbose mode, header/body/auth flag đầy đủ), HTTPie (syntax thân thiện cho dev, color output, plugin), Postman (collection, environment variable, scripting), Bruno (open source alternative giữ file .bru trong git). So sánh nhanh, khi nào dùng tool nào trong workflow phát triển Shop API.