---
title: "Thiết kế API giữa các service - error model, versioning và contract test"
description: "Khoá học về thiết kế API giữa các service, xoay quanh contract: mọi thứ trong request và response mà consumer đã viết code dựa vào. Bạn chọn status code và error model, phân trang bằng cursor, nhận ra breaking change, tắt version cũ theo số liệu, chặn lỗi bằng contract test, rồi áp dụng cho gRPC và GraphQL."
canonical: "https://200lab.io/courses/thiet-ke-api-giua-cac-service-error-model-versioning-va-contract-test"
type: "course"
course_type: "course"
learning_type: "free_form"
published: "2026-10-05T02:56:08Z"
students: 0
---

## Khoá học này mang lại gì cho bạn

API của một service sớm muộn cũng có những consumer mà team bạn không kiểm soát: service của team khác, app mobile đang nằm trên máy người dùng, phần mềm của đối tác. Consumer nào cũng viết code dựa vào status code, tên field, body lỗi và thứ tự của danh sách mà API trả về, rồi deploy theo lịch của riêng mình. Bảy kỹ năng dưới đây giúp bạn thiết kế API cho những consumer đó, và đổi API mà code của họ vẫn chạy đúng.

- **Thiết kế resource và chọn method** — Bạn dựng resource theo việc consumer cần làm thay vì theo từng bảng database, để màn hình chi tiết đơn chỉ cần **1 request** thay vì **10**. Hành động như xác nhận hay hủy đơn được đưa vào một POST tới resource con, vì GET có thể bị bộ quét link hay trình duyệt gọi bất cứ lúc nào.
- **Báo lỗi theo đúng loại** — Bạn chọn status code theo loại kết quả, dùng một hàm duy nhất để map mọi loại lỗi của use case thành body theo Problem Details có `code` ổn định cho code của consumer đọc, và kiểm tra payload theo một schema chặt chẽ để phân biệt lỗi định dạng (400) với lỗi nghiệp vụ (422).
- **Phân trang danh sách và trả 202** — Bạn phân trang bằng keyset cursor trên một khóa sắp xếp duy nhất và không đổi trong lúc đọc, để bản ghi rời khỏi danh sách giữa hai lần đọc không còn đẩy các bản ghi khác lệch trang như khi dùng offset. Với thao tác chạy lâu, bạn trả 202 kèm status resource để consumer hỏi được tiến độ, hủy được và tải được kết quả trước khi kết quả hết hạn.
- **Đổi API mà consumer cũ vẫn chạy** — Bạn giữ contract trong một file OpenAPI được review cùng code, nhìn từ phía từng consumer để nhận ra thay đổi nào là breaking change, chạy song song version cũ qua một lớp chuyển đổi, chọn ngày tắt version cũ từ số request của từng API key, và chặn breaking change ngay trong CI bằng contract test.
- **Truyền context và lần theo request** — Bạn lấy principal và tenant từ token đã xác thực rồi truyền xuống mọi tầng và mọi lời gọi giữa các service. Bạn gắn correlation ID vào mỗi request và ghi log có cấu trúc, để tìm đủ log của một request trong vài phút thay vì vài giờ.
- **Thiết kế contract cho webhook gửi đi** — Khi nền tảng gửi sự kiện cho đối tác, bạn giữ ID sự kiện ổn định qua mọi lần gửi lại, ký từng request bằng HMAC và công bố lịch retry, để đối tác chống trùng, chặn request giả mạo, và biết nếu endpoint của mình ngừng phản hồi thì nền tảng còn gửi lại trong bao lâu.
- **Viết contract gRPC, GraphQL và chọn giao thức** — Bạn thêm field vào file `.proto` mà client cũ vẫn đọc đúng, đặt deadline và status code cho lời gọi gRPC, chia đều tải giữa các replica dù connection HTTP/2 được giữ mở rất lâu, giữ schema GraphQL tương thích và giới hạn chi phí của một query. Sau đó bạn chọn REST, gRPC hay GraphQL cho từng loại consumer bằng số đo.

Bốn nhóm bài đầu dùng HTTP với JSON, nhóm cuối áp dụng cùng những cách thiết kế đó cho gRPC và GraphQL; khoá học không gắn với một framework web, công cụ OpenAPI hay thư viện contract test cụ thể nào.

## Khoá học cung cấp những nền tảng nào

Cả khoá học dựa trên một khái niệm: **contract** của API, tức mọi thứ trong request và response mà consumer nhìn thấy và code của họ đang dựa vào. Contract gồm URL và method của từng endpoint, status code trong từng trường hợp, tên, kiểu và giá trị của từng field (kể cả tập giá trị của một enum), body lỗi, header, thứ tự sắp xếp và cách phân trang. Use case, repository hay câu SQL phía sau không thuộc contract, nên team bạn đổi chúng lúc nào cũng được; còn contract chỉ đổi được theo lịch của consumer.

Mỗi bài trả lời năm câu hỏi về một phần của contract: triệu chứng nào cho thấy phần đó đang sai, contract đúng có dạng ra sao, consumer nhìn thấy thay đổi gì, tradeoff là gì, và ở quy mô nào API chưa cần cơ chế mà bài trình bày. Bài blog [APIs as infrastructure: future-proofing Stripe with versioning](https://stripe.com/blog/api-versioning) cho thấy một công ty mà API là sản phẩm giữ tương thích với mọi version từ năm 2011 ra sao, bài [Consumer-Driven Contracts: A Service Evolution Pattern](https://martinfowler.com/articles/consumerDrivenContracts.html) trên trang của Martin Fowler là nền tảng cho cách tìm ra consumer nào đang dựa vào phần nào của contract, còn [Google Cloud API Design Guide](https://cloud.google.com/apis/design) là bộ hướng dẫn thiết kế áp dụng cho cả HTTP API theo resource lẫn gRPC.

Năm nhóm bài dưới đây đi theo thứ tự một API lớn dần, từ một endpoint đơn lẻ tới nhiều giao thức. Bốn nhóm cuối đều khép lại bằng một bài về một nghiệp vụ cụ thể cần cùng lúc các cơ chế của nhóm, có số đo trước và sau khi đổi contract.

### 1. Resource, method, status code và error model

Màn hình chi tiết đơn trong app của người bán gửi **10 request** nối đuôi nhau, vì API được dựng theo từng bảng: đơn, món, sản phẩm của từng món, giao hàng, thanh toán. p99 thời gian tải màn hình là **3,1 giây**. Khi `GET /orders/{id}` trả một representation có sẵn các món, thông tin giao hàng và thanh toán ở dạng app dùng, màn hình chỉ còn **1 request** và p99 xuống khoảng **0,5 giây**; tradeoff là response lớn hơn và consumer khác có thể nhận cả những field họ không cần.

Method cũng là một phần của contract, vì phần mềm trung gian dựa vào ngữ nghĩa của nó. Email xác nhận đơn COD chứa một link GET làm đổi trạng thái đơn, trong khi bộ lọc email của một số doanh nghiệp tự mở mọi link để quét mã độc: **4%** đơn COD được xác nhận trong vòng **10 giây** sau khi email được gửi, và tỉ lệ hoàn hàng của nhóm đơn này là **31%**. GET là safe method nên phần mềm nào cũng được phép gọi nó bất cứ lúc nào. Vì vậy việc xác nhận đơn được chuyển sang `POST /orders/{id}/confirmation`, và request này chỉ được gửi khi khách bấm nút trên trang.

Status code là phần mà gateway, dashboard, thư viện HTTP và logic retry của client đọc được mà không cần hiểu body. API tạo đơn trả `200 OK` kèm `"success": false` khi từ chối đơn, nên dashboard ghi **99,6%** response 2xx, còn phần mềm của một đối tác ghi **1.440 đơn** mỗi ngày là “đã đặt” dù nền tảng không có đơn nào tương ứng. Ở bài về error model, mọi loại lỗi đi qua một hàm map duy nhất ở tầng ngoài cùng và ra một body theo Problem Details (RFC 9457). Trong body đó, `code` ổn định là một phần của contract; `title` giữ nguyên cho cùng một loại lỗi và chỉ đổi khi được dịch, còn `detail` mô tả riêng từng lần lỗi nên team viết lại lúc nào cũng được. Stack trace hay câu SQL thì ở lại trong log, kèm `request_id`.

Nhóm này kết thúc bằng validation. Đối tác gửi `"price": "199.000"`, API đọc giá bằng `parseFloat` nên lưu thành **199 đồng**, và trong **40 phút** có **2.300 sản phẩm** lên sàn với giá đó. Schema chặt chẽ khai báo giá là số nguyên tính bằng đồng và thời điểm theo RFC 3339 có múi giờ, không tự ép kiểu; khi payload sai schema, API trả 400 kèm mọi lỗi theo field trong cùng một response. Luật nghiệp vụ nằm trong use case, và khi luật bị vi phạm thì API trả 422.

### 2. Cursor cho danh sách dài, trả 202 cho thao tác chạy lâu

Một job lọc spam đọc **60.000 review** đang chờ duyệt bằng offset qua **120 request**, request nào cũng nhận 200, vậy mà vẫn sót **1.100 review** (**1,8%**). Trong **25 phút** quét, **2.300 review** rời khỏi danh sách, và mỗi review rời đi ở phần đã quét kéo một review phía sau lên phần job đã đọc xong. Keyset cursor đánh dấu bản ghi cuối đã đọc bằng một khóa sắp xếp duy nhất và không đổi trong lúc đọc, như `(created_at, id)`, nên bản ghi rời đi không làm lệch trang sau; tradeoff là consumer không nhảy thẳng được tới trang N, và cursor cũ phải có hạn dùng.

Thao tác chạy lâu cần một contract khác. `POST /coupon-batches` tạo **200.000 mã** giảm giá mất khoảng **3 phút**, gateway trả 504 sau **60 giây**, đối tác gửi lại, và cả tháng có **23 lô** bị tạo hai lần cùng **37 ticket** hỏi lô đã xong chưa. Với contract đúng, API trả `202 Accepted` ngay, kèm header `Location` trỏ tới một status resource có trạng thái, tiến độ, link tới kết quả và thời điểm kết quả hết hạn; consumer hủy thao tác bằng một POST tới resource con. Tradeoff là consumer phải poll.

Bài cuối nhóm bắt đầu từ một file CSV **180 MB** chứa **300.000 sản phẩm**: sau khi nhập, người bán thấy **41.000 sản phẩm** bị tạo trùng, còn **4.200 dòng** lỗi không được báo ở đâu cả. Request được xử lý đồng bộ, gateway trả 504 sau **60 giây**, người bán gửi lại thêm bốn lần, và process API còn bị restart hai lần giữa chừng. Bài ghép 202 với validation và error model của nhóm 1 để trả lời hai câu hỏi: vì sao vẫn có **41.000 bản trùng** khi code đã tìm SKU trước khi ghi, và contract phải ra sao để người bán biết dòng nào lỗi, vì lý do gì.

### 3. Đổi API mà không làm hỏng consumer đang gọi

Muốn biết một lần đổi có làm hỏng code của consumer hay không, trước hết contract phải nằm trong một file. Team xuất kho viết client theo một trang wiki được sửa lần cuối **8 tháng** trước, gặp **4 trên 11 field** lệch với response thật, và mất **2 tuần** để sửa. Bài đầu nhóm trình bày cách đưa contract vào một file OpenAPI được review cùng code, so sánh spec-first với code-first, và cách phát hiện drift (spec lệch code) bằng test đối chiếu response thật với spec.

Breaking change phải xét từ phía consumer. Nền tảng thêm giá trị `partially_refunded` vào field `status` của đơn và coi đó chỉ là “thêm”, nhưng parser của **2 trong 140 đối tác** ném lỗi ở nhánh `default`: job đồng bộ đơn của họ dừng **6 giờ** và **1.900 đơn** bị giao trễ. Bài lập bảng các thay đổi thường gặp, chỉ ra thay đổi nào an toàn với consumer nào, và vì sao contract phải ghi từ sớm rằng consumer cần chịu được giá trị lạ (tolerant reader).

Hai bài tiếp theo dành cho lúc buộc phải đổi: chạy song song hai version, rồi tắt version cũ. Hai tháng sau khi phát hành app bản mới, **38%** lượt mở vẫn đến từ bản cũ, nên muốn đổi `address` từ chuỗi sang object thì phải chạy song song hai version: code chỉ giữ một model mới, còn response v1 được dựng từ model đó qua một lớp chuyển đổi ở tầng ngoài cùng. Một lần tắt v1 đúng ngày đã báo trước sáu tháng vẫn gây **4.100 request** lỗi trong giờ đầu, vì access log không ghi API key nên không ai biết đối tác nào còn gọi v1. Khi làm deprecation dựa trên số liệu, team đếm request theo version và theo từng API key, báo cho consumer ngay trong response bằng header `Deprecation` và `Sunset`, rồi chạy brownout (tắt thử version cũ trong một khung giờ ngắn đã báo trước) trước khi tắt hẳn.

Contract test chặn breaking change trước khi deploy. Service giá đổi tên field `discount` thành `discount_amount`; test của service giá xanh, test của checkout cũng xanh vì mock vẫn trả field cũ, và trong **2 giờ** có **3.100 đơn** bị tính nguyên giá. Với consumer-driven contract test, checkout ghi lại đúng những field nó dùng, và CI của service giá chạy các contract đó trước khi deploy. Bài so sánh cách này với hai cách khác: đối chiếu hai bản OpenAPI trong CI, và chạy end-to-end test.

Nhóm khép lại bằng báo cáo đối soát tháng của một đối tác dùng ERP: trên tổng số **420.000 đơn**, ERP thiếu **1.260 đơn** và nhập hai lần **2.100 đơn**. Phần trùng có nguyên nhân đã gặp ở nhóm 2, còn phần thiếu chỉ lộ ra khi lần theo log của từng đơn bị thiếu. Cách sửa phần thiếu làm đổi hình dạng response, nên team phải phát hành `/v2/orders`. Mỗi lần đồng bộ, đối tác đọc lùi một khoảng trước mốc mà lần đồng bộ trước đã đọc tới, và team chọn khoảng đó dài bao nhiêu phút dựa trên giới hạn thời gian của transaction và của mỗi lần đồng bộ. Sau đó team dùng lại version, deprecation và contract test của nhóm này để chuyển đối tác từ v1 sang v2.

### 4. Request đi qua nhiều service và webhook gửi ra cho đối tác

Audit log ghi **37 lần** một người bán đọc báo cáo đơn hàng của người bán khác trong **2 ngày**, vì service báo cáo tin `seller_id` trên query string với lý do “gateway đã kiểm tra token rồi”. Principal và tenant được xác định một lần ở chỗ request đi vào, từ token đã xác thực, rồi được truyền xuống use case như một tham số riêng; repository nhận tenant làm tham số bắt buộc của mọi truy vấn, và job chạy nền cũng mang theo tenant context.

Một đối tác báo “đơn 88124 tạo lúc **10:03** nhận 500”, và kỹ sư trực mất **3 giờ** ghép log của gateway và bốn service theo timestamp. Correlation ID được gán một lần ở gateway, đi giữa các service trong header `traceparent` theo W3C Trace Context, nằm trong field `request_id` của mọi dòng log có cấu trúc, và được trả về cho đối tác trong response để đối tác ghi kèm khi mở ticket. Với cùng sự cố đó, việc tìm đủ log của request chỉ còn mất chưa tới **2 phút**.

Với webhook, chiều gọi đảo ngược: nền tảng là client, đối tác là server. Nền tảng gửi khoảng **80.000 webhook** mỗi ngày; một đối tác cộng doanh thu ba lần cho cùng một sự kiện “đã giao” vì mỗi lần gửi lại mang một ID mới, một đối tác khác nhận một request giả mạo vì webhook không có chữ ký, và cả tuần có **2,1%** sự kiện bị xử lý trùng. Contract cho webhook gửi đi gồm ID sự kiện ổn định qua mọi lần gửi lại trong header `webhook-id`, chữ ký HMAC-SHA256 với secret riêng cho từng endpoint, timestamp chỉ được chấp nhận trong **5 phút**, và lịch retry được công bố cho đối tác.

### 5. Contract gRPC, contract GraphQL và cách chọn giao thức

Nhóm cuối đặt lại những câu hỏi về contract, lỗi và breaking change cho hai giao thức khác. Một pull request bỏ một field trong message `TaxResult` rồi đánh số lại các field còn lại cho gọn; không có lỗi 5xx nào, nhưng khoảng một phần ba số hóa đơn bị ghi sai tiền thuế, đúng phần đi qua các instance của service hóa đơn còn chạy bản build cũ. Bài đầu nhóm tìm ra vì sao client cũ đọc sai mà không báo lỗi, và chỉ ra thay đổi nào trong file `.proto` còn an toàn với client cũ. Hai bài gRPC tiếp theo trình bày cách đặt deadline cho mọi lời gọi, cách map loại lỗi của use case sang status code của gRPC để client biết lỗi nào được retry, và vì sao thêm replica rồi mà một replica vẫn ở mức **90%** CPU: load balancer tầng 4 chia connection, không chia request.

Ở phía GraphQL, một pull request trong BFF của app người bán (backend riêng cho một app) đổi field `shipping` sang non-null. Khoảng **8%** đơn là đơn nhận tại cửa hàng, không có thông tin giao hàng, nên toàn bộ object `order` của các đơn đó thành `null`, trong khi **100%** response vẫn là HTTP 200 và lỗi nằm trong `errors[]` mà app không đọc. Bài tiếp theo xử lý chi phí của một query: màn hình tổng quan gây ra **651 truy vấn** xuống Postgres cho một lần mở, nên cần giảm số truy vấn đó và giới hạn chi phí của một query ngay từ trước khi query chạy.

Sau khi đã thấy contract của cả ba giao thức, bạn chọn giao thức cho từng loại consumer bằng cách nhân số byte và CPU tiết kiệm được với số request/giây của chính consumer đó. Bài cuối khoá đưa cách chọn này vào service gợi ý sản phẩm: service trang sản phẩm gọi nó **3.000 request/giây**, và bước serialize JSON chiếm **38%** thời gian CPU. Bài trình bày cách chuyển service này sang gRPC trong khi service giỏ hàng và service email vẫn gọi bằng JSON theo lịch deploy của riêng họ.

## Vì sao 200Lab tạo ra khoá học này

Hiếm API nào chỉ có một consumer trong thời gian dài. Vài tháng sau khi ra đời, API bạn viết cho frontend của team đã có thêm service của team khác gọi vào, rồi app mobile, rồi phần mềm của đối tác. Mỗi consumer viết code dựa vào đúng những gì API trả về hôm đó, và từ lúc ấy, đổi một tên field hay thêm một giá trị enum không còn là việc riêng của team bạn.

Lỗi do đổi contract hiếm khi hiện ra ở phía bạn. API vẫn trả 200, dashboard vẫn xanh, test vẫn xanh; code hỏng nằm trong parser của đối tác hay trong app bản cũ trên máy người dùng, và bạn chỉ biết qua ticket, qua báo cáo đối soát cuối tháng, hoặc khi phải bật lại version cũ giữa buổi sáng. Lúc đó bạn là người ngồi ghép log của nhiều service để tìm một request, người giải thích với đối tác vì sao doanh thu bị cộng ba lần, và người phải báo cho đối tác một ngày tắt mới, vì lần trước bạn đã phải bật lại version cũ giữa chừng.

Phần lớn cơ chế giữ cho consumer chạy đúng lại khá nhỏ: một field `code` ổn định trong body lỗi, một cột API key trong access log, một contract test trong CI của provider, một header `traceparent` đi theo mỗi lời gọi. Phần khó là những quyết định quanh chúng: thay đổi nào là breaking change với consumer nào, version cũ phải giữ bao lâu, và cơ chế nào chưa cần ở quy mô API hiện tại của bạn.

Học xong khoá học, mỗi lần đổi API của bạn bắt đầu bằng ba câu hỏi bạn tự trả lời được trước khi merge: consumer nào đang đọc phần này, họ deploy lúc nào, và thay đổi có làm code của họ hỏng không. Bạn có số liệu theo từng API key để chọn ngày tắt version cũ, có một ID để tìm đủ log khi đối tác mở ticket, và có cơ sở để nói versioning hay contract test là chưa cần, khi consumer duy nhất là frontend cùng team, cùng repo, được test qua code thật của API và luôn deploy cùng lúc với API.

## Những vấn đề thường gặp khi thiết kế API giữa các service

Khi API có vấn đề, nhiều team tìm ở phía mình trước: tỉ lệ 2xx trên dashboard, test của service, log của chính service. Với API giữa các service, chỗ hỏng thường nằm ở nơi khác: trong code của consumer, trong một phần contract chưa ai ghi lại, hay trên đường đi của lời gọi giữa các service.

- Dashboard ghi **99,6%** response 2xx, nhưng phần mềm của một đối tác ghi **1.440 đơn** mỗi ngày là “đã đặt” trong khi nền tảng không có đơn nào tương ứng — API trả `200 OK` kèm `"success": false` khi từ chối đơn, mà thư viện HTTP của đối tác coi mọi 2xx là thành công.
- Chỉ dịch câu thông báo lỗi sang tiếng Việt mà tỉ lệ đơn bỏ dở của một đối tác tăng từ **3%** lên **11%** trong **2 ngày** — body lỗi không có `code` ổn định, nên code của đối tác nhận ra lỗi hết hàng bằng cách so sánh từng chữ của `message`.
- **4%** đơn COD được xác nhận trong vòng **10 giây** sau khi email được gửi, trước khi khách kịp mở thư — link xác nhận là một GET làm đổi trạng thái đơn, và bộ lọc email của doanh nghiệp tự mở mọi link để quét mã độc.
- Job lọc spam gửi đủ **120 request**, request nào cũng nhận 200, vậy mà vẫn sót **1.100 review** — offset đếm vị trí trên một danh sách đang thay đổi, nên mỗi review rời khỏi phần đã quét kéo một review phía sau lên phần job đã đọc xong.
- Chỉ thêm một giá trị enum `partially_refunded` mà job đồng bộ đơn của hai đối tác dừng **6 giờ** — contract chưa ghi rằng consumer phải chịu được giá trị lạ, và parser của hai đối tác ném lỗi ở nhánh `default`.
- Test của service giá và service checkout đều xanh **100%**, vậy mà **3.100 đơn** bị tính nguyên giá — mock trong test của checkout vẫn trả field `discount` theo bản cũ, nên không test nào thấy field đã đổi tên.
- Tắt v1 đúng ngày đã báo trước sáu tháng, rồi phải bật lại lúc **10:40** — access log không ghi API key, nên trước ngày tắt không ai biết vẫn còn **23 đối tác** đang gọi v1.
- Thêm hai replica cho service gRPC mà một replica vẫn **90%** CPU, hai replica kia chỉ khoảng **1%** — load balancer tầng 4 chia connection chứ không chia request, còn client gửi mọi lời gọi trên một connection HTTP/2 mở từ lúc khởi động.

Mỗi dòng trên là phần mở đầu của một bài trong khoá; ở đó bạn đọc request và response thật, tìm ra phần contract gây lỗi, rồi chọn cách đổi kèm tradeoff.

## Bạn sẽ học theo cách nào

- **Bắt đầu từ triệu chứng thật** — Mỗi bài mở bằng một tín hiệu bạn có thể gặp khi trực: một response body, vài dòng access log, tỉ lệ 2xx trên dashboard hay báo cáo đối soát của đối tác, kèm câu hỏi chuyện gì đang xảy ra.
- **Contract viết ra đầy đủ, kèm hình minh họa** — Contract trước và sau khi đổi được thể hiện bằng request và response HTTP thật, file `.proto` hay schema GraphQL; ảnh minh họa và sơ đồ gọn trong mỗi bài cho thấy đường đi của request, chuỗi 202 và polling, hay các lần gửi lại của một webhook.
- **Mỗi bài chốt một quyết định cho contract** — Status code, field, version, ngày tắt version cũ hay giao thức đều được chọn từ số liệu của ví dụ, kèm tradeoff, và mỗi bài nói rõ ở quy mô nào API chưa cần cơ chế của bài.
- **Quiz yêu cầu bạn dự đoán** — Nhiều bài có quiz ngắn, như đoán consumer cũ đọc ra gì sau khi field bị đánh số lại, hay một thay đổi có phải breaking change hay không.
- **Bốn demo kit chạy bằng Docker** — Bài cuối của nhóm 2, 3, 4 và 5 đều có một kit dạng file Markdown cho coding agent của bạn: agent dựng hệ thống ở trạng thái “trước” bằng ngôn ngữ bạn chọn và chạy load test với một mức tải cố định để triệu chứng hiện ra, rồi đo lại từng tiêu chí sau khi bạn tự đổi contract theo bài. Kit không chứa lời giải và không bắt buộc.

## Khoá học dành cho ai

1. **Developer backend làm API cho team khác** — Service của bạn đã có consumer là service của team khác hoặc app mobile, và bạn cần đổi API mà không phải hẹn từng team deploy cùng ngày với mình.
2. **Người phụ trách API cho đối tác** — Hàng chục hay hàng trăm đối tác gọi vào API của bạn và nhận webhook từ nền tảng, và bạn cần biết đối tác nào còn dùng phần cũ trước khi tắt nó.
3. **Developer có service gọi API của team khác** — Service của bạn từng hỏng vì team khác đổi API, và bạn cần khai báo những field mà service của mình đang dùng, để CI của team kia chặn trước khi deploy những pull request đổi tên, đổi kiểu hay xóa các field đó.
4. **Người đang cân nhắc gRPC hay GraphQL** — Team bạn có đề xuất đổi giao thức, và bạn cần tiêu chí đo được để quyết định cho từng loại consumer thay vì đổi cả nền tảng cùng lúc.

## Những điều cần lưu ý

- **Cần kiến thức nền về chia tầng và idempotency key** — Bạn cần biết trước cách chia một service thành handler, use case và repository (use case nhận command và trả result, còn handler chuyển request thành command, chuyển result thành response và phân biệt 400 với 422), cùng idempotency key và request ID. Bài nhập file CSV và bài webhook còn dùng thêm checkpoint, worker nhận và giữ job bằng lease, outbox, at-least-once và cách nhận và kiểm tra webhook; mỗi kiến thức này chỉ được nhắc lại trong một câu, để phần lớn bài dành cho contract và cách đổi contract.
- **Không gồm retry, rate limiting hay bảo mật** — Retry và backoff, rate limiting phía server, contract của event và message, đổi schema database trên hệ thống đang chạy, OAuth, JWT, mTLS, WebSocket và cách cấu hình một API gateway cụ thể không có trong khoá học. Tên công cụ như Pact, buf hay Apollo chỉ xuất hiện ở phần tham khảo; bạn hiểu cơ chế đủ để biết mỗi công cụ đang làm hộ bạn quyết định nào.
- **Số liệu trong ví dụ do bài tự đặt** — Số đối tác, tỉ lệ lỗi hay số byte mỗi response trong các bài được chọn để các phép tính khớp nhau, không phải số đo từ production của một công ty nào; khi áp dụng, bạn đo trên API của mình, như đếm request theo API key hay lấy profile CPU của bước serialize, rồi đưa vào cùng cách quyết định.

## Bắt đầu từ đâu

Khoá học mở đầu bằng một sáng thứ Hai sau bản deploy cuối tuần: dashboard vẫn ghi **99,7%** response 2xx, còn ticket của đối tác tăng từ **4** lên **31**. Bài đầu tiên trả lời vì sao bốn sự cố của buổi sáng đó không hiện trên dashboard của API, contract gồm những phần nào, và API của bạn đang ở nấc nào trong bốn nấc quy mô, chia theo số consumer tự deploy theo lịch riêng của họ. Những câu trả lời đó cho biết nhóm bài nào bạn cần ngay, và cơ chế nào còn chưa cần.

## Chương trình

- Vì sao đổi API lại khó hơn nhiều so với đổi code bên trong service? (miễn phí)

### Resource, method, status code và error model

- Một màn hình cần mười request vì API được dựng theo từng bảng (miễn phí)
- Chọn HTTP method để công cụ quét link không xác nhận đơn thay khách (miễn phí) · quiz
- API trả 200 cho cả request bị từ chối · quiz
- Sửa message lỗi làm code của đối tác không nhận ra lỗi hết hàng
- Kiểm tra payload theo schema để dữ liệu sai kiểu không lọt vào database · quiz

### Cursor cho danh sách dài, trả 202 cho thao tác chạy lâu

- Phân trang bằng offset bỏ sót bản ghi khi danh sách đang thay đổi · quiz
- Trả 202 kèm status resource để consumer biết thao tác chạy lâu đã xong chưa · quiz
- Thiết kế API nhập 300.000 sản phẩm từ một file CSV · kit cho agent

### Đổi API mà không làm hỏng consumer đang gọi

- Đưa contract của API vào một file được review cùng code
- Vì sao chỉ thêm một giá trị enum cũng có thể là breaking change · quiz
- Đổi kiểu dữ liệu của một field khi app bản cũ vẫn còn trên máy người dùng · quiz
- Tắt version API cũ dựa trên số request của từng API key
- Test của hai service đều xanh mà service checkout vẫn tính sai giá · quiz
- Đối tác đồng bộ đơn hàng qua API mà vẫn thiếu đơn · kit cho agent

### Request đi qua nhiều service và webhook gửi ra cho đối tác

- Người bán đọc được đơn hàng của người bán khác qua API nội bộ · quiz
- Dùng correlation ID để gom log của một request đi qua nhiều service · quiz
- Thiết kế contract cho webhook mà nền tảng gửi tới đối tác · kit cho agent

### Contract gRPC, contract GraphQL và cách chọn giao thức

- Đánh số lại field trong file .proto làm hóa đơn ghi sai tiền thuế · quiz
- Service đơn hàng treo theo service tồn kho vì lời gọi gRPC không có deadline · quiz
- Vì sao thêm replica mà mọi lời gọi gRPC vẫn dồn vào một replica · quiz
- Đổi một field GraphQL sang non-null làm cả object cha thành null · quiz
- Một query GraphQL gây ra hàng trăm truy vấn xuống database · quiz
- Chọn REST, gRPC hay GraphQL cho từng loại consumer
- Chuyển API của service gợi ý sản phẩm từ REST sang gRPC · kit cho agent
