Thiết kế API công khai thực dụng cho người xây dựng SaaS lần đầu: chọn versioning, phân trang, giới hạn tốc độ, tài liệu và một SDK nhỏ để phát hành nhanh.

API công khai không chỉ là một endpoint ứng dụng của bạn phơi ra. Đó là một cam kết với những người ngoài nhóm rằng hợp đồng sẽ tiếp tục hoạt động ngay cả khi bạn thay đổi sản phẩm.
Khó khăn không phải là viết v1. Khó là giữ nó ổn định trong khi bạn sửa bug, thêm tính năng, và học xem khách hàng thực sự cần gì.
Những lựa chọn ban đầu sẽ hiện ra sau này dưới dạng ticket hỗ trợ. Nếu phản hồi thay đổi mà không báo trước, nếu đặt tên không nhất quán, hoặc client không biết một request có thành công hay không, bạn tạo ra ma sát. Ma sát đó biến thành mất niềm tin, và khi mất niềm tin, người ta ngừng xây dựng trên nền tảng của bạn.
Tốc độ cũng quan trọng. Hầu hết người xây dựng SaaS lần đầu cần phát hành thứ gì đó hữu dụng nhanh, rồi cải thiện dần. Đổi lại là rõ ràng: càng phát hành nhanh mà không có quy tắc, bạn càng tốn thời gian undo các quyết định đó khi người dùng thật đến.
"Đủ tốt cho v1" thường có nghĩa là một tập nhỏ endpoints phản ánh hành động thực của người dùng, tên và cấu trúc phản hồi nhất quán, chiến lược thay đổi rõ ràng (ngay cả khi chỉ là v1), phân trang dự đoán được và giới hạn tốc độ hợp lý, cùng tài liệu cho thấy chính xác gửi gì và nhận lại điều gì.
Một ví dụ cụ thể: tưởng tượng một khách hàng xây integration tạo hóa đơn mỗi đêm. Nếu sau này bạn đổi tên một trường, thay đổi định dạng ngày, hoặc âm thầm bắt đầu trả partial results, công việc của họ sẽ lỗi lúc 2 giờ sáng. Họ sẽ đổ lỗi cho API của bạn, không phải code của họ.
Nếu bạn xây bằng công cụ chat như Koder.ai, sẽ rất hấp dẫn khi sinh nhiều endpoint nhanh. Điều đó ổn, nhưng giữ bề mặt public nhỏ. Bạn có thể giữ endpoint nội bộ private trong khi học xem phần nào nên trở thành hợp đồng lâu dài.
Thiết kế API công khai tốt bắt đầu bằng việc chọn một tập nhỏ danh từ (resources) khớp với cách khách hàng nói về sản phẩm của bạn. Giữ tên resource ổn định ngay cả khi database nội bộ thay đổi. Khi thêm tính năng, ưu tiên thêm trường hoặc endpoint mới hơn là đổi tên resource lõi.
Một tập bắt đầu thực tế cho nhiều sản phẩm SaaS là: users, organizations, projects, và events. Nếu bạn không thể giải thích một resource trong một câu, có lẽ nó chưa sẵn sàng để công khai.
Giữ việc sử dụng HTTP nhàm chán và dự đoán được:
Auth không cần quá phức tạp ngày đầu. Nếu API của bạn chủ yếu server-to-server (khách gọi từ backend của họ), API keys thường đủ. Nếu khách cần hành động như người dùng cuối cá nhân, hoặc bạn kỳ vọng tích hợp bên thứ ba nơi người dùng cấp quyền, OAuth thường phù hợp hơn. Viết quyết định bằng ngôn ngữ rõ ràng: ai là caller, và họ được phép chạm vào dữ liệu của ai?
Đặt kỳ vọng sớm. Rõ ràng điều gì được hỗ trợ và điều gì là cố gắng tốt nhất. Ví dụ: các endpoint list là ổn định và tương thích ngược, nhưng bộ lọc tìm kiếm có thể mở rộng và không đảm bảo đầy đủ. Điều này giảm ticket hỗ trợ và giúp bạn tự do cải thiện.
Nếu bạn xây trên nền tảng vibe-coding như Koder.ai, hãy coi API như một sản phẩm hợp đồng: giữ hợp đồng nhỏ trước, rồi mở rộng dựa trên sử dụng thực, không phải đoán mò.
Versioning chủ yếu về kỳ vọng. Client muốn biết: integration của tôi có bị phá tuần sau không? Bạn muốn có không gian để cải thiện mà không sợ.
Versioning theo header có thể trông sạch, nhưng dễ bị ẩn trong logs, cache, và ảnh chụp màn hình hỗ trợ. Versioning trong URL thường là lựa chọn đơn giản nhất: /v1/.... Khi khách gửi request lỗi, bạn có thể thấy version ngay. Nó cũng giúp chạy v1 và v2 song song dễ dàng.
Thay đổi là phá vỡ nếu một client cư xử đúng có thể ngừng hoạt động mà không cần thay đổi code. Ví dụ thường gặp:
customer_id thành customerId)Một thay đổi an toàn là thay đổi mà client cũ có thể bỏ qua. Thêm một trường tùy chọn thường an toàn. Ví dụ, thêm plan_name vào phản hồi GET /v1/subscriptions sẽ không phá vỡ client chỉ đọc status.
Một quy tắc thực tế: đừng xóa hoặc tái sử dụng trường trong cùng một major version. Thêm trường mới, giữ trường cũ, và chỉ retire khi bạn sẵn sàng deprecate toàn bộ version.
Giữ đơn giản: thông báo sớm, trả về message cảnh báo rõ trong phản hồi, và đặt ngày kết thúc. Với API đầu tiên, cửa sổ 90 ngày thường thực tế. Trong thời gian đó, giữ v1 hoạt động, xuất một ghi chú migration ngắn, và đảm bảo support có thể chỉ vào một câu: v1 hoạt động đến ngày này; đây là điều khác trong v2.
Nếu bạn xây trên nền tảng như Koder.ai, coi các version API như snapshots: phát hành cải tiến trong version mới, giữ cũ ổn định, và chỉ cắt khi đã cho khách hàng thời gian di chuyển.
Phân trang là nơi giành được hoặc mất niềm tin. Nếu kết quả nhảy lung tung giữa các request, người ta ngừng tin API của bạn.
Dùng page/limit khi dataset nhỏ, query đơn giản, và người dùng thường muốn trang 3 của 20. Dùng cursor khi list có thể lớn, mục mới đến thường xuyên, hoặc người dùng có thể sort và filter nhiều. Cursor giữ thứ tự ổn định ngay cả khi bản ghi mới được thêm.
Một vài quy tắc giúp phân trang đáng tin:
Tổng số (totals) khó xử lý. total_count có thể tốn kém trên bảng lớn, đặc biệt với filters. Nếu bạn có thể cung cấp rẻ, hãy bao gồm. Nếu không, bỏ nó hoặc làm tùy chọn qua query flag.
Dưới đây là các hình dạng request/response đơn giản.
// Page/limit
GET /v1/invoices?page=2&limit=25&sort=created_at_desc
{
"items": [{"id":"inv_1"},{"id":"inv_2"}],
"page": 2,
"limit": 25,
"total_count": 142
}
// Cursor-based
GET /v1/invoices?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDozMDowMFoiLCJpZCI6Imludl8xMDAifQ==
{
"items": [{"id":"inv_101"},{"id":"inv_102"}],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDoyNTowMFoiLCJpZCI6Imludl8xMjUifQ=="
}
Giới hạn tốc độ không phải chỉ để nghiêm ngặt mà để giữ hệ thống trực tuyến. Chúng bảo vệ app khỏi traffic spikes, database khỏi các query tốn kém lặp lại quá thường, và ví của bạn khỏi hóa đơn hạ tầng bất ngờ. Giới hạn cũng là một hợp đồng: client biết sử dụng bình thường trông như thế nào.
Bắt đầu đơn giản và tinh chỉnh sau. Chọn mức bao phủ sử dụng điển hình với dư địa cho burst ngắn, rồi quan sát traffic thực. Nếu chưa có dữ liệu, mặc định an toàn là giới hạn theo API key như 60 requests/phút cộng một allowance burst nhỏ. Nếu một endpoint nặng hơn nhiều (như search hoặc exports), áp giới hạn chặt hơn hoặc quy tắc chi phí riêng thay vì phạt mọi request.
Khi bạn áp limit, hãy làm cho client dễ làm điều đúng. Trả về 429 Too Many Requests và bao gồm vài header tiêu chuẩn:
X-RateLimit-Limit: tối đa cho windowX-RateLimit-Remaining: còn bao nhiêuX-RateLimit-Reset: khi window reset (timestamp hoặc seconds)Retry-After: nên chờ bao lâu trước khi thử lạiClient nên coi 429 là điều bình thường, không phải lỗi phải chiến đấu. Mẫu retry lịch sự giữ cả hai bên hài lòng:
Retry-After khi cóVí dụ: nếu khách chạy sync hàng đêm gây tải nặng, job của họ có thể dàn requests trong một phút và tự chậm lại khi gặp 429 thay vì làm hỏng toàn bộ chạy.
Nếu lỗi API khó đọc, ticket hỗ trợ sẽ tăng nhanh. Chọn một hình dạng lỗi và dùng nó mọi nơi, kể cả 500s. Một chuẩn đơn giản là: code, message, details, và request_id để người dùng dán vào chat hỗ trợ.
Đây là một định dạng nhỏ, dễ đoán:
{
"error": {
"code": "validation_error",
"message": "Some fields are invalid.",
"details": {
"fields": [
{"name": "email", "issue": "must be a valid email"},
{"name": "plan", "issue": "must be one of: free, pro, business"}
]
},
"request_id": "req_01HT..."
}
}
Dùng HTTP status codes cùng cách mọi lần: 400 cho input sai, 401 khi thiếu hoặc auth không hợp lệ, 403 khi đã auth nhưng không được phép, 404 khi resource không tìm thấy, 409 cho conflicts (ví dụ duplicate unique hoặc state sai), 429 cho rate limits, và 500 cho lỗi server. Tính nhất quán thắng sự sáng tạo.
Làm cho lỗi validation dễ sửa. Gợi ý theo trường nên trỏ tới tên parameter mà docs dùng, không phải cột database nội bộ. Nếu có yêu cầu format (date, currency, enum), nói rõ những gì bạn chấp nhận và đưa ví dụ.
Retries là nơi nhiều API vô tình tạo dữ liệu trùng lặp. Với các POST quan trọng (payments, tạo invoice, gửi email), hỗ trợ idempotency keys để client retry an toàn.
Idempotency-Key trên các endpoint POST được chọn.Header đó ngăn nhiều edge case đau đầu khi mạng không ổn hoặc client timeout.
Giả sử bạn chạy SaaS đơn giản với ba đối tượng chính: projects, users, và invoices. Một project có nhiều user, và mỗi project nhận hóa đơn hàng tháng. Client muốn sync invoices vào công cụ kế toán của họ và hiển thị billing cơ bản trong app của họ.
Một v1 sạch có thể trông như sau:
GET /v1/projects/{project_id}
GET /v1/projects/{project_id}/invoices
POST /v1/projects/{project_id}/invoices
Bây giờ xảy ra một breaking change. Trong v1, bạn lưu tiền là integer cents: amount_cents: 1299. Sau đó bạn cần multi-currency và decimals, nên muốn amount: "12.99" và currency: "USD". Nếu bạn ghi đè trường cũ, mọi integration hiện có sẽ vỡ. Versioning tránh hoảng loạn: giữ v1 ổn định, phát hành /v2/... với trường mới, và hỗ trợ cả hai cho đến khi client migrate.
Với listing invoices, dùng phân trang dự đoán. Ví dụ:
GET /v1/projects/p_123/invoices?limit=50&cursor=eyJpZCI6Imludl85OTkifQ==
200 OK
{
"data": [ {"id":"inv_1001"}, {"id":"inv_1000"} ],
"next_cursor": "eyJpZCI6Imludl8xMDAwIn0="
}
Một ngày khách import invoices trong vòng lặp và gặp giới hạn. Thay vì lỗi ngẫu nhiên, họ nhận được phản hồi rõ:
429 Too Many RequestsRetry-After: 20{ "error": { "code": "rate_limited" } }Bên họ, client có thể pause 20 giây, rồi tiếp tục từ cùng cursor mà không tải lại mọi thứ hoặc tạo invoice trùng.
Phát hành v1 tốt hơn khi bạn coi nó như một release sản phẩm nhỏ, không phải một đống endpoints. Mục tiêu đơn giản: mọi người có thể xây trên đó, và bạn có thể cải thiện mà không có bất ngờ.
Bắt đầu bằng việc viết một trang giải thích API dành cho gì và không dành cho gì. Giữ bề mặt nhỏ đủ để bạn có thể giải thích bằng lời trong một phút.
Dùng trình tự này và đừng chuyển bước cho đến khi mỗi bước đủ tốt:
Nếu bạn xây bằng workflow sinh code (ví dụ dùng Koder.ai để scaffold endpoints và responses), vẫn làm fake-client test. Code sinh có thể trông đúng nhưng vẫn khó dùng.
Lợi tức là ít email hỗ trợ hơn, ít hotfix, và một v1 bạn thực sự có thể duy trì.
SDK đầu tiên không phải là một sản phẩm thứ hai. Hãy coi nó như một wrapper mỏng, thân thiện quanh HTTP API. Nó nên làm các gọi phổ biến dễ, nhưng không che giấu cách API hoạt động. Nếu ai đó cần tính năng bạn chưa bọc, họ vẫn nên có thể quay về gọi HTTP thô.
Chọn một ngôn ngữ để bắt đầu, dựa vào ngôn ngữ khách hàng thực sự dùng. Với nhiều API B2B SaaS, thường là JavaScript/TypeScript hoặc Python. Phát hành một SDK tốt hơn ba SDK nửa vời.
Một tập khởi đầu tốt là:
Bạn có thể viết bằng tay hoặc sinh từ OpenAPI spec. Sinh mã tuyệt khi spec chính xác và bạn muốn typing đồng nhất, nhưng thường sinh ra nhiều code. Ban đầu, một client tối giản viết tay cộng file OpenAPI cho docs thường đủ. Bạn có thể chuyển sang client sinh sau mà không phá vỡ người dùng, miễn là interface SDK công khai ổn định.
API version theo quy tắc tương thích. SDK version theo quy tắc đóng gói.
Nếu bạn thêm param tùy chọn hoặc endpoints mới, đó thường là bump nhỏ cho SDK. Dành major SDK release cho breaking change trong SDK (đổi tên method, đổi default), ngay cả khi API không thay đổi. Sự tách này giữ nâng cấp trơn tru và ít ticket hỗ trợ.
Hầu hết ticket API không phải do bug. Làm người dùng bất ngờ mới là vấn đề. Thiết kế API công khai phần lớn là làm cho mọi thứ nhàm chán và dự đoán được để code client chạy liên tục tháng này qua tháng khác.
Cách nhanh nhất để mất niềm tin là thay đổi phản hồi mà không nói ai. Nếu bạn đổi tên trường, thay đổi kiểu, hoặc bắt đầu trả null thay vì giá trị, bạn sẽ phá vỡ client theo cách khó chẩn đoán. Nếu phải thay đổi behavior, version nó, hoặc thêm trường mới và giữ trường cũ một thời gian với kế hoạch sunset rõ ràng.
Phân trang là lỗi lặp lại khác. Vấn đề xuất hiện khi một endpoint dùng page/pageSize, endpoint khác dùng offset/limit, và endpoint thứ ba dùng cursors, tất cả với default khác nhau. Chọn một mẫu cho v1 và dùng cho mọi nơi. Giữ sort ổn định để trang sau không bỏ sót hoặc lặp khi bản ghi mới xuất hiện.
Lỗi gây nhiều trao đổi khi không nhất quán. Một chế độ thất bại phổ biến là một service trả { "error":"..." } và service khác trả { "message":"..." }, với status code khác nhau cho cùng một vấn đề. Client rồi phải xây handler rối rắm theo endpoint.
Đây là năm sai lầm gây ra nhiều email nhất:
Một thói quen đơn giản giúp: mỗi phản hồi nên bao gồm một request_id, và mỗi 429 nên giải thích khi nào retry.
Trước khi xuất bản, rà soát cuối cùng tập trung vào tính nhất quán. Phần lớn ticket hỗ trợ xảy ra vì chi tiết nhỏ không khớp giữa endpoints, docs, và ví dụ.
Kiểm tra nhanh bắt được phần lớn vấn đề:
Sau khi ra mắt, theo dõi những gì người ta thực sự gọi, không phải những gì bạn mong họ dùng. Bảng điều khiển nhỏ và review hàng tuần là đủ sớm.
Theo dõi các tín hiệu này trước:
Thu thập phản hồi mà không rewrite mọi thứ. Thêm đường báo lỗi ngắn trong docs, và gắn mỗi báo cáo với endpoint, request id, và client version. Khi bạn sửa, ưu tiên các thay đổi additive: trường mới, param tùy chọn, hoặc endpoint mới, thay vì phá vỡ behavior hiện có.
Bước tiếp: viết spec API một trang với resources, kế hoạch versioning, quy tắc phân trang, và định dạng lỗi. Sau đó xuất docs và một SDK khởi tạo bao gồm auth + 2–3 endpoint lõi. Nếu muốn nhanh hơn, bạn có thể phác thảo spec, docs, và SDK khởi tạo từ một kế hoạch chat sử dụng công cụ như Koder.ai (chế độ planning của nó tiện để map endpoints và ví dụ trước khi sinh code).
Bắt đầu với 5–10 endpoints liên quan trực tiếp đến hành động của khách hàng.
Một quy tắc hay: nếu bạn không thể giải thích một resource trong một câu (nó là gì, ai sở hữu nó, cách dùng ra sao), hãy giữ nó ở nội bộ cho đến khi bạn có dữ liệu sử dụng rõ ràng.
Chọn một tập nhỏ các danh từ ổn định (resources) mà khách hàng đã dùng khi nói về sản phẩm, và giữ tên đó ổn định ngay cả khi cơ sở dữ liệu nội bộ thay đổi.
Các starter phổ biến cho SaaS là users, organizations, projects, và events — chỉ thêm khi có nhu cầu rõ ràng.
Dùng ý nghĩa chuẩn của HTTP và nhất quán:
GET = đọc (không có side effects)POST = tạo hoặc bắt đầu một hành độngPATCH = cập nhật một vài trườngDELETE = xóa hoặc vô hiệu hóaLợi ích chính là tính dự đoán: client không nên đoán phương thức sẽ làm gì.
Mặc định chọn version trong URL như /v1/....
Nó dễ nhìn trong logs và ảnh chụp màn hình, dễ debug với khách hàng, và đơn giản khi chạy v1 và v2 song song khi cần thay đổi phá vỡ.
Một thay đổi là phá vỡ nếu một client đúng đắn có thể ngừng hoạt động mà không cần chỉnh code. Ví dụ thông thường:
Thêm một trường tùy chọn thường an toàn.
Giữ đơn giản:
Một mặc định thực tế cho API đầu tiên là 90 ngày để khách hàng có thời gian di chuyển.
Chọn một mẫu và dùng nó cho tất cả endpoint list.
Luôn định nghĩa sort mặc định và tie-breaker (ví dụ created_at + ) để kết quả không nhảy lung tung.
Bắt đầu với giới hạn rõ ràng theo key (ví dụ 60 requests/phút + burst nhỏ), sau đó điều chỉnh theo traffic thực tế.
Khi giới hạn, trả về 429 và bao gồm:
X-RateLimit-LimitX-RateLimit-RemainingDùng một định dạng lỗi duy nhất mọi nơi (kể cả 500). Cấu trúc thực dụng gồm:
code (identifier ổn định)message (dễ đọc)details (vấn đề theo trường)request_id (cho hỗ trợ)Giữ status codes nhất quán (400/401/403/404/409/429/500) để client xử lý dễ dàng.
Nếu bạn sinh nhiều endpoint nhanh (ví dụ bằng Koder.ai), giữ bề mặt công khai nhỏ và xem đó là hợp đồng dài hạn.
Trước khi ra mắt:
POST quan trọngSau đó xuất một SDK nhỏ giúp auth, timeout, retry cho request an toàn, và phân trang — nhưng không che giấu cách hoạt động HTTP.
idX-RateLimit-ResetRetry-AfterĐiều này giúp client retry có dự đoán và giảm ticket hỗ trợ.