API Versioning — Chiến lược quản lý phiên bản API cho Production

Posted on: 4/25/2026 4:13:34 AM

API Versioning là một trong những quyết định kiến trúc quan trọng nhất khi xây dựng hệ thống microservices. Một chiến lược versioning tốt cho phép bạn phát triển API liên tục mà không phá vỡ hàng nghìn client đang hoạt động. Bài viết này phân tích sâu các pattern, so sánh cách tiếp cận của Stripe, GitHub, Google, và hướng dẫn triển khai thực tế trên .NET 10.

83% API public dùng URL versioning (2026)
6-12 tháng — thời gian deprecation khuyến nghị
4 chiến lược versioning phổ biến
24+ tháng GitHub hỗ trợ API version cũ

1. Tại sao API Versioning quan trọng?

Khi hệ thống phát triển, API không thể đứng yên. Yêu cầu nghiệp vụ thay đổi, data model tiến hóa, và bạn cần sửa những thiết kế sai từ đầu. Nhưng mỗi thay đổi breaking change có thể làm sập hàng nghìn ứng dụng client đang phụ thuộc vào API của bạn.

API Versioning giải quyết bài toán này bằng cách cho phép nhiều phiên bản API cùng tồn tại song song, giúp client chuyển đổi theo lộ trình thay vì bị ép buộc.

Breaking Change là gì?

Là bất kỳ thay đổi nào khiến client hiện tại bị lỗi: xóa field, đổi tên field, thay đổi kiểu dữ liệu, xóa endpoint, thay đổi HTTP status code, hoặc thay đổi business logic mà response khác đi. Ngược lại, thêm field mới vào response thường KHÔNG phải breaking change nếu client bỏ qua field không biết.

2. Bốn chiến lược Versioning phổ biến

2.1. URL Path Versioning

Phiên bản được nhúng trực tiếp vào đường dẫn URL. Đây là phương pháp phổ biến nhất, được dùng bởi Twitter, Facebook, và hầu hết các API công khai.

GET /api/v1/products/123
GET /api/v2/products/123

Ưu điểm: Dễ hiểu, dễ test trên browser, cache-friendly vì URL khác nhau tạo cache key khác nhau, dễ routing ở API Gateway.

Nhược điểm: URL thay đổi khi version mới ra, phá vỡ nguyên tắc "URI đại diện cho resource". Nhiều version dẫn đến nhiều route trùng lặp.

2.2. Query String Versioning

GET /api/products/123?api-version=1.0
GET /api/products/123?api-version=2.0

Ưu điểm: URL base giữ nguyên, dễ thêm version vào request hiện có. Client chỉ cần append thêm query parameter.

Nhược điểm: Dễ quên truyền version, cần xử lý default version. Một số CDN không cache tốt khi có query string.

2.3. Header Versioning

GET /api/products/123
X-Api-Version: 1.0

GET /api/products/123
X-Api-Version: 2.0

Ưu điểm: URL sạch hoàn toàn, tách biệt concern giữa resource identity và version. Phù hợp khi API phục vụ nhiều client khác nhau cần version khác nhau trên cùng resource.

Nhược điểm: Không test được trên browser trực tiếp, khó debug, client cần cấu hình header cho mọi request.

2.4. Date-Based Versioning (Stripe/GitHub style)

GET /v1/charges
Stripe-Version: 2026-04-22

GET /api/repos
X-GitHub-Api-Version: 2026-03-10

Ưu điểm: Rất chi tiết — mỗi ngày release có thể là một version. Client được pin vào thời điểm cụ thể, đảm bảo behavior không thay đổi. Stripe tự động pin account vào version khi tạo lần đầu.

Nhược điểm: Phức tạp hơn khi triển khai — cần duy trì tầng tương thích (compatibility layer) cho mọi version cũ. Chỉ phù hợp với team có quy trình vận hành API chuyên nghiệp.

graph TD
    A["Client Request"] --> B{"Version Detection"}
    B -->|URL Path| C["/api/v1/products"]
    B -->|Query String| D["/api/products?v=1.0"]
    B -->|Header| E["X-Api-Version: 1.0"]
    B -->|Date-Based| F["Stripe-Version: 2026-04-22"]
    C --> G["Version Router"]
    D --> G
    E --> G
    F --> G
    G --> H["Controller v1"]
    G --> I["Controller v2"]
    G --> J["Compatibility Layer"]
    style A fill:#e94560,stroke:#fff,color:#fff
    style B fill:#2c3e50,stroke:#fff,color:#fff
    style G fill:#16213e,stroke:#fff,color:#fff
    style H fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style I fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style J fill:#f8f9fa,stroke:#e94560,color:#2c3e50

Luồng xử lý API Version Detection

3. So sánh chi tiết các chiến lược

Tiêu chí URL Path Query String Header Date-Based
Độ phổ biến Rất cao (83%) Trung bình Thấp Thấp (chuyên biệt)
Dễ hiểu Rất dễ Dễ Trung bình Khó
Cache-friendly Tốt Trung bình Cần cấu hình Vary Cần cấu hình Vary
URL sạch Không Không
Test trên browser Dễ Dễ Không thể Không thể
Độ chi tiết version Major only Major.Minor Tùy chỉnh Rất chi tiết (ngày)
Phù hợp API công khai API nội bộ API B2B Fintech, PaaS

4. Case Study: Stripe và GitHub

4.1. Stripe — Date-Based Versioning tinh vi

Stripe sử dụng hệ thống versioning dựa trên ngày, với mỗi version là một chuỗi ngày (ví dụ 2026-04-22). Khi developer tạo tài khoản Stripe lần đầu, API version tại thời điểm đó sẽ được tự động pin vào account. Mọi request từ account đó sẽ sử dụng version đó, trừ khi override bằng header Stripe-Version.

Cách tiếp cận này đảm bảo rằng code của developer không bao giờ tự nhiên bị hỏng — dù Stripe release hàng trăm thay đổi mỗi năm. Khi muốn nâng cấp, developer chủ động upgrade version trên dashboard hoặc qua header.

Bài học từ Stripe

Stripe duy trì một compatibility layer khổng lồ — mỗi khi có breaking change mới, họ viết một "version change" transformer biến response mới thành format cũ cho các version cũ hơn. Các transformer này được chain lại: request v2024-01-01 sẽ đi qua tất cả transformer từ phiên bản hiện tại ngược về 2024-01-01. Đây là mô hình cực kỳ hiệu quả nhưng đòi hỏi kỷ luật engineering cao.

4.2. GitHub — Date-Based với lộ trình 24 tháng

GitHub REST API sử dụng version dựa trên ngày (ví dụ 2026-03-10), truyền qua header X-GitHub-Api-Version. Mỗi version được hỗ trợ ít nhất 24 tháng sau khi version mới ra. GitHub cũng công bố rõ ràng danh sách breaking changes cho từng version trên documentation.

5. Triển khai trên ASP.NET Core với Asp.Versioning

Thư viện Asp.Versioning (trước đây là Microsoft.AspNetCore.Mvc.Versioning) là giải pháp chính thức cho API versioning trên .NET. Nó hỗ trợ tất cả 4 chiến lược versioning và tích hợp sâu với ASP.NET Core.

5.1. Cài đặt

dotnet add package Asp.Versioning.Http
dotnet add package Asp.Versioning.Mvc.ApiExplorer

5.2. Cấu hình cơ bản

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Api-Version"),
        new QueryStringApiVersionReader("api-version")
    );
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

5.3. Controller versioning

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        return Ok(new { Id = id, Name = "Widget", Price = 29.99 });
    }
}

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class ProductsV2Controller : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        return Ok(new
        {
            Id = id,
            Name = "Widget",
            Price = new { Amount = 29.99, Currency = "USD" },
            Category = "Electronics",
            CreatedAt = DateTime.UtcNow
        });
    }
}

5.4. Minimal API versioning

var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .ReportApiVersions()
    .Build();

app.MapGet("/api/v{version:apiVersion}/products/{id}", (int id) =>
    Results.Ok(new { Id = id, Name = "Widget" }))
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(new ApiVersion(1, 0));

app.MapGet("/api/v{version:apiVersion}/products/{id}", (int id) =>
    Results.Ok(new { Id = id, Name = "Widget",
        Price = new { Amount = 29.99, Currency = "USD" } }))
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(new ApiVersion(2, 0));

5.5. Deprecation với Sunset Header

[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}"), MapToApiVersion("1.0")]
    public IActionResult GetV1(int id) => Ok(new { Id = id });

    [HttpGet("{id}"), MapToApiVersion("2.0")]
    public IActionResult GetV2(int id) => Ok(new { Id = id, Category = "Electronics" });
}

Khi Deprecated = true, ASP.NET Core tự động thêm header api-deprecated-versions: 1.0 vào mọi response. Bạn có thể bổ sung thêm middleware để inject Sunset header (RFC 8594):

app.Use(async (context, next) =>
{
    await next();
    var feature = context.Features.Get<IApiVersioningFeature>();
    if (feature?.RequestedApiVersion?.MajorVersion == 1)
    {
        context.Response.Headers["Sunset"] = "Sat, 01 Nov 2026 00:00:00 GMT";
        context.Response.Headers["Deprecation"] = "true";
        context.Response.Headers["Link"] =
            "<https://api.example.com/docs/migration-v2>; rel=\"successor-version\"";
    }
});
sequenceDiagram
    participant C as Client
    participant GW as API Gateway
    participant R as Version Router
    participant V1 as Controller v1 (deprecated)
    participant V2 as Controller v2

    C->>GW: GET /api/v1/products/1
    GW->>R: Route to v1
    R->>V1: Handle request
    V1-->>C: 200 OK + Sunset: 2026-11-01
    Note over C: Client nhận cảnh báo deprecation

    C->>GW: GET /api/v2/products/1
    GW->>R: Route to v2
    R->>V2: Handle request
    V2-->>C: 200 OK (full response)

Luồng deprecation với Sunset header

6. API Versioning tại tầng API Gateway

Trong kiến trúc microservices, API Gateway đóng vai trò trung tâm trong việc quản lý version. Thay vì mỗi service tự xử lý versioning, Gateway có thể route request đến đúng phiên bản service.

graph LR
    C["Client"] --> GW["API Gateway
(YARP / Kong)"] GW -->|v1/*| S1A["Service A - v1"] GW -->|v2/*| S1B["Service A - v2"] GW -->|v1/*| S2["Service B - v1"] GW -->|v2/*| S2B["Service B - v2"] S1A --> DB1["Database"] S1B --> DB1 S2 --> DB2["Database"] S2B --> DB2 style GW fill:#e94560,stroke:#fff,color:#fff style C fill:#2c3e50,stroke:#fff,color:#fff style S1A fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50 style S1B fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style S2 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50 style S2B fill:#f8f9fa,stroke:#e94560,color:#2c3e50

API Gateway routing theo version

6.1. YARP (Yet Another Reverse Proxy) trên .NET

{
  "Routes": {
    "products-v1": {
      "ClusterId": "products-v1",
      "Match": { "Path": "/api/v1/products/{**catch-all}" }
    },
    "products-v2": {
      "ClusterId": "products-v2",
      "Match": { "Path": "/api/v2/products/{**catch-all}" }
    }
  },
  "Clusters": {
    "products-v1": {
      "Destinations": {
        "primary": { "Address": "https://products-v1.internal:5001/" }
      }
    },
    "products-v2": {
      "Destinations": {
        "primary": { "Address": "https://products-v2.internal:5002/" }
      }
    }
  }
}

6.2. Hai chiến lược triển khai multi-version

Chiến lược Mô tả Khi nào dùng
Cùng codebase Tất cả version trong 1 project, dùng [ApiVersion] attribute để phân biệt Thay đổi nhỏ, ít breaking change
Tách service Mỗi version là 1 deployment riêng, Gateway route theo version Thay đổi lớn, schema khác nhau hoàn toàn

7. Quản lý Breaking Changes

7.1. Additive Change Strategy

Nguyên tắc vàng: luôn thêm, không bao giờ xóa hoặc đổi. Nếu tuân thủ quy tắc này, bạn gần như không cần tạo version mới.

  • Thêm field mới vào response — KHÔNG breaking
  • Thêm optional parameter vào request — KHÔNG breaking
  • Thêm endpoint mới — KHÔNG breaking
  • Xóa field khỏi response — BREAKING
  • Đổi tên field — BREAKING
  • Thay đổi kiểu dữ liệu (string → object) — BREAKING
  • Thay đổi HTTP status code — BREAKING

7.2. Compatibility Layer Pattern (Stripe-style)

Thay vì duy trì nhiều bộ code cho mỗi version, bạn viết code mới nhất và tạo transformer chuyển đổi response ngược về format cũ:

public interface IVersionTransformer
{
    ApiVersion TargetVersion { get; }
    object Transform(object currentResponse);
}

public class ProductV1Transformer : IVersionTransformer
{
    public ApiVersion TargetVersion => new(1, 0);

    public object Transform(object currentResponse)
    {
        if (currentResponse is ProductV2Response v2)
        {
            return new ProductV1Response
            {
                Id = v2.Id,
                Name = v2.Name,
                Price = v2.Price.Amount // v2 có Price object, v1 chỉ có decimal
            };
        }
        return currentResponse;
    }
}
graph LR
    A["Request v1"] --> B["Latest Controller"]
    B --> C["Response v3 format"]
    C --> D["Transformer v3→v2"]
    D --> E["Transformer v2→v1"]
    E --> F["Response v1 format"]
    style A fill:#e94560,stroke:#fff,color:#fff
    style B fill:#2c3e50,stroke:#fff,color:#fff
    style F fill:#4CAF50,stroke:#fff,color:#fff
    style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50

Compatibility Layer — chain transformer ngược từ version mới nhất về version cũ

8. Lộ trình Deprecation chuyên nghiệp

Tháng 0 — Announce
Công bố version mới, thêm Deprecated = true cho version cũ. Gửi email cho developers, cập nhật changelog.
Tháng 1-3 — Grace Period
Response version cũ kèm header Sunset với ngày cụ thể. Theo dõi traffic — bao nhiêu % client vẫn dùng version cũ?
Tháng 3-6 — Active Migration
Gửi reminder cho client chưa migrate. Cung cấp migration guide chi tiết. Bật warning log cho mọi request version cũ.
Tháng 6-12 — Final Notice
Thông báo ngày chính xác sẽ tắt version cũ. Giảm rate limit cho version cũ để khuyến khích migrate.
Tháng 12+ — Sunset
Trả về 410 Gone cho version cũ, kèm link documentation hướng dẫn nâng cấp.

9. Versioning cho GraphQL API

GraphQL theo triết lý "versionless" — không có version rõ ràng. Thay vào đó, schema tiến hóa liên tục thông qua:

  • Thêm field mới — client chỉ query những field cần, field mới không ảnh hưởng
  • Deprecate field bằng directive @deprecated(reason: "Use priceV2 instead")
  • Không bao giờ xóa field — chỉ deprecate và để lại vô thời hạn hoặc đến khi không còn client nào query
type Product {
  id: ID!
  name: String!
  price: Float @deprecated(reason: "Use priceV2 for currency support")
  priceV2: Money!
  category: String!
}

type Money {
  amount: Float!
  currency: String!
}

GraphQL vs REST Versioning

Với GraphQL, bạn gần như không cần versioning vì client tự chọn field. Nhưng nếu có thay đổi behavior (không chỉ schema), bạn vẫn cần một cơ chế version — ví dụ dùng header X-Schema-Version hoặc tách endpoint /graphql/v2.

10. Anti-Patterns cần tránh

Những sai lầm phổ biến

1. Quá nhiều version: Duy trì 5+ version đồng thời là ác mộng. Stripe là ngoại lệ vì họ có team chuyên trách. Hầu hết team nên giới hạn 2-3 version hoạt động.

2. Version tất cả khi chưa cần: Không cần tạo v2 nếu chỉ thêm field mới. Additive changes không cần version mới.

3. Bỏ qua default version: Khi AssumeDefaultVersionWhenUnspecified = false, client cũ không truyền version sẽ nhận 400 Bad Request. Luôn set true trong giai đoạn chuyển đổi.

4. Không theo dõi usage: Nếu không biết bao nhiêu client đang dùng v1, bạn không biết khi nào an toàn để tắt nó.

5. Breaking change không thông báo: Thay đổi behavior mà không tăng version là vi phạm contract nghiêm trọng nhất.

11. Monitoring và Observability

Theo dõi usage của từng API version là bước quan trọng để ra quyết định deprecation. Tích hợp OpenTelemetry để ghi nhận metric:

app.Use(async (context, next) =>
{
    var version = context.GetRequestedApiVersion()?.ToString() ?? "unspecified";
    var meter = context.RequestServices.GetRequiredService<Meter>();
    var counter = meter.CreateCounter<long>("api.requests.by_version");
    counter.Add(1, new KeyValuePair<string, object?>("api.version", version));
    await next();
});

Với metric này, bạn có thể tạo dashboard Grafana hiển thị:

  • Phân bổ traffic theo version (% v1 vs v2)
  • Trend migration — v1 traffic giảm dần theo thời gian
  • Alert khi version deprecated vẫn còn traffic cao sau deadline

12. Kết luận

API Versioning không chỉ là quyết định kỹ thuật — nó là cam kết với developer đang dùng API của bạn rằng code của họ sẽ không tự nhiên bị hỏng. Chọn chiến lược phù hợp với quy mô team và đối tượng client:

  • API công khai, đội nhỏ → URL Path Versioning + giới hạn 2-3 version
  • API nội bộ microservices → Header Versioning + API Gateway routing
  • API fintech/platform lớn → Date-Based Versioning (Stripe model)
  • GraphQL → Schema evolution + @deprecated directive

Dù chọn chiến lược nào, hãy luôn ưu tiên additive changes, triển khai Sunset header khi deprecate, và theo dõi usage bằng OpenTelemetry. API tốt là API mà developer tin tưởng — và versioning chính là nền tảng của sự tin tưởng đó.

Tham khảo