OpenAPI-first vs code-first API development: những đánh đổi chính
So sánh OpenAPI-first và code-first: tốc độ, tính nhất quán, sinh client và biến lỗi validate thành thông báo rõ ràng cho người dùng.

Vấn đề thực sự cuộc tranh luận này cố gắng giải quyết
Tranh luận OpenAPI-first vs code-first không thực sự chỉ là sở thích. Nó là về ngăn chặn sự trôi dần giữa những gì API tuyên bố làm và những gì nó thực sự làm.
OpenAPI-first nghĩa là bạn bắt đầu bằng việc viết hợp đồng API (endpoints, input, output, lỗi) trong một spec OpenAPI, sau đó xây server và các client để khớp với nó. Code-first nghĩa là bạn viết API bằng mã trước, rồi sinh hoặc viết spec OpenAPI và tài liệu từ hiện thực.
Các đội tranh luận vì sự đau xuất hiện sau đó, thường là một app client hỏng sau một thay đổi "nhỏ" ở backend, tài liệu mô tả hành vi server không còn có, quy tắc validate không nhất quán giữa các endpoint, lỗi 400 mơ hồ buộc người ta phải đoán, và ticket hỗ trợ bắt đầu bằng "hôm qua còn được".
Một ví dụ đơn giản: app mobile gửi phoneNumber, nhưng backend đổi tên trường thành phone. Server trả về 400 chung chung. Tài liệu vẫn nói phoneNumber. Người dùng thấy "Bad Request" và dev phải mò log.
Câu hỏi thực sự là: làm sao bạn giữ hợp đồng, hành vi runtime, và kỳ vọng client đồng bộ khi API thay đổi?
So sánh này tập trung vào bốn kết quả ảnh hưởng đến công việc hàng ngày: tốc độ (cái giúp bạn ship nhanh bây giờ và cái giữ nhanh sau này), tính nhất quán (hợp đồng, docs và hành vi runtime trùng khớp), sinh client (khi spec tiết kiệm thời gian và ngăn lỗi), và lỗi xác thực (làm sao biến "invalid input" thành thông báo người dùng có thể hành động).
Hai luồng làm việc: OpenAPI-first và code-first thường vận hành thế nào
OpenAPI-first bắt đầu từ hợp đồng. Trước khi ai đó viết mã endpoint, nhóm thống nhất các path, dạng request/response, status code, và định dạng lỗi. Ý tưởng đơn giản: quyết định API sẽ trông thế nào, rồi xây để khớp.
Một flow OpenAPI-first điển hình:
- Soạn spec OpenAPI (endpoints, schemas, auth, lỗi)
- Review với backend, frontend, và QA
- Sinh stub hoặc chia sẻ spec như nguồn chân lý
- Triển khai server để khớp
- Validate request và response theo hợp đồng (tests hoặc middleware)
Code-first đảo ngược thứ tự. Bạn xây endpoint trong mã, rồi thêm annotation hoặc comment để một công cụ sinh ra tài liệu OpenAPI sau đó. Cảm giác nhanh hơn khi đang thử nghiệm vì bạn có thể thay logic và route ngay mà không cần cập nhật spec riêng.
Một flow code-first điển hình:
- Triển khai endpoint và model trong code
- Thêm annotation cho schema, params, responses
- Sinh spec OpenAPI từ codebase
- Chỉnh đầu ra (thường bằng cách tinh annotation)
- Dùng spec sinh ra cho docs và sinh client
Chỗ drift xuất hiện phụ thuộc vào workflow. Với OpenAPI-first, drift xảy ra khi spec được đối xử như tài liệu thiết kế một lần và ngừng được cập nhật sau đó. Với code-first, drift xảy ra khi mã thay đổi nhưng annotation không, nên spec sinh ra trông đúng còn hành vi thực (status code, trường bắt buộc, các trường hợp biên) lặng lẽ thay đổi.
Một quy tắc đơn giản: contract-first drift khi spec bị bỏ qua; code-first drift khi tài liệu là thứ bị nghĩ đến sau cùng.
Tốc độ: cái cảm thấy nhanh bây giờ vs cái giữ nhanh sau này
Tốc độ không chỉ một thứ. Có "bao lâu ta có thể ship thay đổi tiếp theo" và "bao lâu ta có thể tiếp tục ship sau 6 tháng thay đổi." Hai cách tiếp cận thường đổi vai cảm giác nhanh.
Ban đầu, code-first có thể cảm thấy nhanh hơn. Bạn thêm trường, chạy app, và nó hoạt động. Khi API còn đang di chuyển, vòng phản hồi này rất khó đánh bại. Chi phí lộ ra khi người khác bắt đầu phụ thuộc vào API: mobile, web, công cụ nội bộ, đối tác, và QA.
OpenAPI-first có thể cảm thấy chậm trên ngày đầu bởi vì bạn viết hợp đồng trước khi endpoint tồn tại. Lợi tức là ít làm lại hơn. Khi đổi tên trường, thay đổi được thấy và review trước khi phá vỡ client.
Tốc độ lâu dài chủ yếu là tránh churn: ít hiểu lầm giữa các nhóm hơn, ít vòng QA do hành vi không nhất quán, onboard nhanh hơn vì hợp đồng là điểm bắt đầu rõ ràng, và phê duyệt sạch hơn vì thay đổi rõ ràng.
Cái làm nhóm chậm nhất không phải là gõ code. Là làm lại: rebuild client, viết lại test, cập nhật docs, và trả lời ticket hỗ trợ do hành vi mơ hồ.
Nếu bạn xây công cụ nội bộ và app mobile song song, contract-first cho phép cả hai nhóm tiến cùng lúc. Và nếu bạn dùng nền tảng tự động sinh code khi yêu cầu thay đổi (ví dụ, AppMaster), cùng nguyên tắc giúp bạn tránh mang theo quyết định cũ khi app tiến triển.
Tính nhất quán: giữ hợp đồng, docs, và hành vi trùng khớp
Hầu hết nỗi đau API không phải do thiếu tính năng. Là do không khớp: docs nói một chuyện, server làm chuyện khác, và client hỏng theo cách khó phát hiện.
Sự khác biệt chính là "nguồn chân lý." Trong luồng contract-first, spec là tham chiếu và mọi thứ khác phải theo. Trong luồng code-first, server chạy là tham chiếu, và spec cùng docs thường theo sau.
Tên, kiểu, và trường bắt buộc là nơi drift xuất hiện đầu tiên. Một trường bị đổi tên trong code nhưng không trong spec. Một boolean trở thành string vì một client gửi "true". Một trường từ optional thành required, nhưng client cũ vẫn gửi dạng cũ. Mỗi thay đổi nhỏ. Cộng lại tạo tải hỗ trợ ổn định.
Cách thực tế để giữ nhất quán là quyết định cái gì không bao giờ được lệch, rồi bắt buộc nó trong workflow:
- Dùng một schema chuẩn cho request và response (bao gồm trường bắt buộc và định dạng).
- Version các thay đổi phá vỡ một cách có chủ ý. Đừng âm thầm đổi nghĩa trường.
- Thống nhất quy tắc đặt tên (snake_case vs camelCase) và áp dụng khắp nơi.
- Đối xử ví dụ như các test có thể chạy, không chỉ tài liệu.
- Thêm kiểm tra hợp đồng trong CI để mismatch bị fail sớm.
Ví dụ xứng đáng được chăm chút vì đó là thứ người ta sao chép. Nếu ví dụ thiếu một trường bắt buộc, bạn sẽ có lưu lượng thực với trường thiếu.
Sinh client: khi nào OpenAPI thực sự có lợi
Client sinh tự động có ích nhất khi hơn một nhóm (hoặc app) tiêu thụ cùng API. Ở đó tranh luận ngừng là sở thích và bắt đầu tiết kiệm thời gian.
Bạn có thể sinh ra gì (và vì sao nó giúp)
Từ một hợp đồng OpenAPI vững bạn có thể sinh nhiều hơn docs. Các đầu ra phổ biến: model có type giúp bắt lỗi sớm, SDK client cho web và mobile (method, types, hook auth), server stub để giữ triển khai khớp, test fixture và sample payload cho QA và support, và mock server để frontend có thể bắt đầu trước khi backend xong.
Lợi tức nhanh nhất khi bạn có web app, mobile app, và có thể công cụ nội bộ đều gọi cùng endpoint. Một thay đổi hợp đồng nhỏ có thể được regen khắp nơi thay vì implement lại tay.
Client sinh vẫn có thể gây bực khi bạn cần tuỳ biến nặng (flow auth đặc biệt, retry, cache offline, upload file) hoặc khi generator tạo mã bạn đội không thích. Một thỏa hiệp phổ biến là sinh core types và client thấp rồi bọc bằng một lớp viết tay mỏng cho app.
Giữ client sinh không bị hỏng âm thầm
Mobile và frontend ghét thay đổi bất ngờ. Để tránh "hôm qua còn compile được":
- Đối xử với hợp đồng như artifact có version và review thay đổi như code.
- Thêm kiểm tra CI fail khi có breaking change (loại bỏ trường, đổi type).
- Ưu tiên thay đổi bổ sung (trường mới optional) và đánh dấu deprecate trước khi xóa.
- Giữ phản hồi lỗi nhất quán để client xử lý dự đoán được.
Nếu đội vận hành có web admin panel và nhân viên hiện trường dùng app native, sinh Kotlin/Swift model từ cùng file OpenAPI ngăn tên trường lệch và enum thiếu.
Lỗi xác thực: biến "400" thành thứ người dùng hiểu được
Hầu hết phản hồi "400 Bad Request" không phải xấu. Chúng là lỗi validate bình thường: thiếu trường bắt buộc, số gửi thành text, hoặc ngày sai định dạng. Vấn đề là output validate thô thường đọc như ghi chú cho dev, không phải thứ người dùng có thể sửa.
Các lỗi sinh nhiều ticket nhất là thiếu trường bắt buộc, sai kiểu, sai định dạng (date, UUID, phone, currency), giá trị ngoài phạm vi, và giá trị không được chấp nhận (ví dụ status không nằm trong danh sách).
Cả hai workflow có thể dẫn đến cùng kết quả: API biết lỗi là gì, nhưng client nhận thông báo mơ hồ như "invalid payload." Sửa điều này ít liên quan đến workflow hơn là áp dụng một dạng lỗi rõ ràng và quy tắc mapping nhất quán.
Một mẫu đơn giản: giữ phản hồi nhất quán và làm mọi lỗi có thể hành động. Trả về (1) trường sai, (2) vì sao sai, và (3) cách sửa.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Please fix the highlighted fields.",
"details": [
{
"field": "email",
"rule": "format",
"message": "Enter a valid email address."
},
{
"field": "age",
"rule": "min",
"message": "Age must be 18 or older."
}
]
}
}
Điều này cũng map tốt đến form UI: bôi nổi trường, hiện message cạnh trường, và giữ một thông điệp ngắn ở đầu cho người bỏ sót. Chìa khóa là tránh lộ ngôn ngữ nội bộ (như "failed schema validation") và thay vào đó dùng ngôn ngữ phù hợp với những gì người dùng có thể thay đổi.
Nơi validate và cách tránh lặp rules
Validate tốt nhất khi mỗi tầng có nhiệm vụ rõ ràng. Nếu mọi tầng cố gắng kiểm enforced mọi quy tắc, bạn sẽ có làm đôi, lỗi gây nhầm lẫn, và rules trôi giữa web, mobile và backend.
Một phân chia thực tế trông như sau:
- Edge (API gateway hoặc request handler): validate shape và types (thiếu trường, sai định dạng, giá trị enum). Đây là nơi schema OpenAPI phù hợp.
- Service layer (business logic): validate quy tắc thực sự (permissions, state transitions, "end date must be after start date", "discount only for active customers").
- Database: enforce những gì không bao giờ được vi phạm (unique constraints, foreign keys, not-null). Xử lý lỗi database như mạng lưới an toàn, không phải trải nghiệm chính cho người dùng.
Để giữ cùng rules giữa web và mobile, dùng một hợp đồng và một định dạng lỗi duy nhất. Ngay cả khi client làm kiểm tra nhanh (như trường bắt buộc), họ vẫn nên dựa vào API làm trọng tài cuối cùng. Bằng cách đó không cần cập nhật mobile chỉ vì một rule thay đổi.
Ví dụ đơn giản: API yêu cầu phone ở định dạng E.164. Edge có thể từ chối format sai nhất quán cho mọi client. Nhưng "phone chỉ được đổi một lần mỗi ngày" thuộc service layer vì phụ thuộc lịch sử người dùng.
Ghi log cái gì vs hiển thị cái gì
Với dev, log đủ để debug: request id, user id (nếu có), endpoint, mã rule validate, tên trường, và exception thô. Với người dùng, giữ ngắn và có thể hành động: trường nào lỗi, sửa gì, và (khi an toàn) một ví dụ. Tránh lộ tên bảng nội bộ, stack trace, hoặc chi tiết policy như "user is not in role X."
Bước từng bước: chọn và triển khai một cách tiếp cận
Nếu nhóm bạn còn tranh luận, đừng cố quyết cho cả hệ thống một lúc. Chọn một lát nhỏ rủi ro thấp và làm thật. Bạn sẽ học nhiều hơn từ một pilot hơn là vài tuần tranh luận.
Bắt đầu với scope chặt: một resource và 1–3 endpoint người ta thật sự dùng (ví dụ, "create ticket", "list tickets", "update status"). Giữ đủ gần production để cảm nhận được đau, nhưng đủ nhỏ để có thể đổi hướng.
Kế hoạch rollout thực tế
-
Chọn pilot và định nghĩa "xong" là gì (endpoints, auth, các case success và failure chính).
-
Nếu chọn OpenAPI-first, viết schema, ví dụ, và dạng lỗi chuẩn trước khi viết mã server. Đối xử spec như thỏa thuận chung.
-
Nếu chọn code-first, xây handler trước, xuất spec, rồi dọn nó (tên, mô tả, ví dụ, phản hồi lỗi) cho đến khi đọc như một hợp đồng.
-
Thêm kiểm tra hợp đồng để mọi thay đổi có chủ ý: fail build nếu spec phá vỡ backward compatibility hoặc nếu client sinh ra drift khỏi hợp đồng.
-
Triển khai cho một client thật (UI web hoặc app mobile), rồi thu thập điểm ma sát và cập nhật rules.
Nếu bạn dùng nền tảng no-code như AppMaster, pilot có thể nhỏ hơn: mô hình dữ liệu, định nghĩa endpoint, và dùng cùng hợp đồng để điều khiển cả màn admin web và view mobile. Công cụ quan trọng ít hơn thói quen: một nguồn chân lý, test cho mỗi thay đổi, với ví dụ khớp payload thật.
Sai lầm phổ biến tạo chậm trễ và ticket hỗ trợ
Hầu hết nhóm không thất bại vì họ chọn phía "sai." Họ thất bại vì họ đối xử với hợp đồng và runtime như hai thế giới riêng, rồi mất cả tuần hòa giải.
Cạm bẫy kinh điển là viết file OpenAPI như "tài liệu đẹp" nhưng không bao giờ thi hành nó. Spec drift, client sinh từ nguồn sai, và QA tìm mismatch muộn. Nếu bạn publish một hợp đồng, hãy làm nó testable: validate request/response theo nó, hoặc sinh server stub giữ hành vi khớp.
Một máy phát ticket khác là sinh client mà không có quy tắc version. Nếu app mobile hoặc client đối tác auto-update SDK mới nhất, một thay đổi nhỏ (như đổi tên trường) trở thành phá vỡ âm thầm. Khóa version client, publish chính sách thay đổi rõ ràng, và đối xử breaking change như release có chủ ý.
Xử lý lỗi là nơi những bất nhất nhỏ tạo chi phí lớn. Nếu mỗi endpoint trả về dạng 400 khác nhau, frontend sẽ có các parser một-off và thông báo chung "Có lỗi gì đó." Chuẩn hóa lỗi để client luôn hiện được văn bản hữu ích.
Các kiểm tra nhanh ngăn hầu hết chậm trễ:
- Giữ một nguồn chân lý: hoặc sinh code từ spec, hoặc sinh spec từ code, và luôn verify chúng khớp.
- Khóa client sinh theo version API, và ghi rõ gì được tính là breaking.
- Dùng một định dạng lỗi ở mọi nơi (cùng trường, cùng ý nghĩa), và bao gồm mã lỗi ổn định.
- Thêm ví dụ cho các trường khó (date format, enum, object lồng nhau), không chỉ type.
- Validate ở biên (gateway hoặc controller), để business logic giả sử input đã sạch.
Kiểm tra nhanh trước khi quyết định hướng
Trước khi chọn hướng, chạy vài kiểm tra nhỏ để lộ các điểm ma sát thật sự trong nhóm.
Checklist sẵn sàng đơn giản
Chọn một endpoint đại diện (request body, rules validate, vài case lỗi), rồi xác nhận bạn có thể trả lời "có" cho những điều sau:
- Có một owner được đặt tên cho hợp đồng và một bước review rõ ràng trước khi thay đổi đẩy.
- Phản hồi lỗi trông và hành xử giống nhau giữa các endpoint: cùng dạng JSON, mã lỗi dự đoán được, và thông điệp người không-ky-thuat cũng hiểu được.
- Bạn có thể sinh client từ hợp đồng và dùng nó trong một màn UI thật mà không phải edit type bằng tay hay đoán tên trường.
- Các thay đổi phá vỡ bị bắt trước khi deploy (diff hợp đồng trong CI, hoặc tests fail khi response không còn khớp schema).
Nếu bạn vấp về ownership và review, bạn sẽ ship API "gần đúng" và nó sẽ drift theo thời gian. Nếu vấp về dạng lỗi, ticket hỗ trợ sẽ chất đống vì người dùng chỉ thấy "400 Bad Request" thay vì "Email bị thiếu" hoặc "Start date phải trước end date."
Một kiểm tra thực tế: lấy một màn form (ví dụ tạo customer) và cố tình submit ba input sai. Nếu bạn có thể biến các lỗi ấy thành thông báo rõ ràng theo từng trường mà không cần code đặc biệt, bạn đã gần có một approach có thể mở rộng.
Kịch bản ví dụ: công cụ nội bộ cộng với app mobile dùng cùng API
Một đội nhỏ xây công cụ admin nội bộ trước, rồi vài tháng sau làm app mobile cho nhân viên hiện trường. Cả hai gọi cùng API: tạo work order, update status, đính kèm ảnh.
Với code-first, admin tool thường chạy sớm vì web UI và backend thay đổi cùng lúc. Vấn đề lộ ra khi mobile app phát hành muộn hơn. Khi đó endpoint drift: một trường bị đổi tên, một giá trị enum thay đổi, và một endpoint bắt đầu yêu cầu một param trước kia là "optional." Đội mobile phát hiện mismatch muộn, thường là 400 ngẫu nhiên, và ticket chất đống vì người dùng chỉ thấy "Có lỗi gì đó.".
Với contract-first, cả admin web và mobile có thể dựa trên cùng dạng, tên, và quy tắc từ ngày đầu. Dù chi tiết triển khai thay đổi, hợp đồng vẫn là tham chiếu chung. Sinh client cũng có lợi hơn: app mobile có thể sinh request và model typed thay vì viết tay và đoán trường bắt buộc.
Validation là nơi người dùng cảm nhận khác biệt nhiều nhất. Giả sử mobile gửi số điện thoại thiếu mã vùng. Phản hồi thô như "400 Bad Request" vô ích. Một lỗi người dùng hiểu được có thể nhất quán giữa nền tảng, ví dụ:
code:INVALID_FIELDfield:phonemessage:Enter a phone number with country code (example: +14155552671).hint:Add your country prefix, then retry.
Thay đổi nhỏ đó biến quy tắc backend thành bước tiếp theo rõ ràng cho người thật, dù họ ở admin tool hay app mobile.
Bước tiếp theo: chọn pilot, chuẩn hóa lỗi, và xây tự tin
Một quy tắc hữu dụng: chọn OpenAPI-first khi API được chia sẻ giữa nhiều nhóm hoặc cần hỗ trợ nhiều client (web, mobile, partners). Chọn code-first khi một nhóm sở hữu mọi thứ và API thay đổi hàng ngày, nhưng vẫn sinh OpenAPI spec từ code để không mất hợp đồng.
Quyết định hợp đồng sống ở đâu và được review thế nào. Thiết lập đơn giản nhất là lưu file OpenAPI trong cùng repo backend và yêu cầu review cho mỗi thay đổi. Giao cho một owner rõ ràng (thường là API owner hoặc tech lead) và include ít nhất một dev client trong review cho các thay đổi có thể phá vỡ app.
Nếu bạn muốn nhanh mà không code mọi thứ tay, cách tiếp cận driven-by-contract cũng phù hợp với nền tảng no-code sinh toàn bộ app từ một thiết kế chung. Ví dụ, AppMaster (appmaster.io) có thể sinh backend và web/mobile từ cùng model nền tảng, giúp giữ hành vi API và kỳ vọng UI đồng bộ khi yêu cầu thay đổi.
Tiến hành với một pilot nhỏ, rồi mở rộng:
- Chọn 2–5 endpoint có người dùng thật và ít nhất một client (web hoặc mobile).
- Chuẩn hóa phản hồi lỗi để một "400" thành thông báo trường rõ ràng (trường nào lỗi và phải làm gì).
- Thêm kiểm tra hợp đồng vào workflow (diff kiểm breaking change, lint cơ bản, và tests xác minh response khớp contract).
Làm tốt ba việc đó, phần còn lại của API sẽ dễ xây hơn, dễ tài liệu hơn, và dễ support hơn.
Câu hỏi thường gặp
Chọn OpenAPI-first khi nhiều client hoặc nhiều nhóm phụ thuộc vào cùng một API, vì hợp đồng trở thành tham chiếu chung và giảm bất ngờ. Chọn code-first khi một nhóm nắm cả server lẫn client và bạn vẫn đang khám phá cấu trúc API, nhưng vẫn nên sinh spec từ mã và giữ nó được review để không mất sự đồng bộ.
Nó xảy ra khi “nguồn chân lý” không được thi hành. Với contract-first, drift xuất hiện khi spec không được cập nhật sau thay đổi. Với code-first, drift xuất hiện khi triển khai thay đổi nhưng annotations và tài liệu sinh ra không phản ánh mã thực tế (mã trạng thái, trường bắt buộc, các trường hợp biên).
Đối xử với hợp đồng như thứ có thể làm fail build. Thêm kiểm tra tự động để so sánh thay đổi hợp đồng xem có khác biệt phá vỡ backward-compatibility không, và thêm tests hoặc middleware để validate request/response theo schema để lỗi được bắt trước khi deploy.
Các client sinh ra có lợi khi nhiều app tiêu thụ cùng API, vì types và chữ ký hàm ngăn lỗi phổ biến như tên trường sai hoặc enum thiếu. Chúng có thể phiền khi cần hành vi tùy chỉnh, nên chiến lược tốt là sinh client cấp thấp rồi bọc bằng một lớp thủ công mỏng mà app của bạn dùng.
Ưu tiên các thay đổi bổ sung như trường mới không bắt buộc và endpoint mới, vì chúng không phá vỡ client hiện tại. Khi phải phá vỡ, version một cách có chủ ý và làm cho thay đổi hiển nhiên trong review; đổi tên âm thầm hoặc đổi kiểu là cách nhanh nhất khiến khách hàng thấy “hôm qua vẫn ổn”.
Dùng một JSON error shape duy nhất trên mọi endpoint và làm mỗi lỗi có thể hành động được: bao gồm mã lỗi ổn định, trường cụ thể (khi phù hợp), và một thông điệp người đọc hiểu được giải thích phải thay đổi gì. Giữ thông điệp đầu ngắn, tránh tiết lộ cụm từ nội bộ như “schema validation failed.”
Validate các kiểu cơ bản, định dạng và giá trị cho phép ở biên (handler, controller, hoặc gateway) để dữ liệu xấu bị chặn sớm và nhất quán. Đưa quy tắc nghiệp vụ vào service layer, và dựa vào database chỉ cho các ràng buộc bắt buộc như unique; lỗi database là mạng lưới an toàn, không phải trải nghiệm người dùng chính.
Ví dụ là thứ người ta sao chép vào request thực, nên ví dụ sai tạo lưu lượng thực sai. Giữ ví dụ nhất quán với các trường bắt buộc và định dạng, và đối xử chúng như test case để còn chính xác khi API thay đổi.
Bắt đầu với một phần nhỏ có người dùng thật, ví dụ một resource với 1–3 endpoint và vài trường hợp lỗi. Định nghĩa “xong” là gì, chuẩn hóa phản hồi lỗi, và thêm kiểm tra hợp đồng trong CI; khi workflow đó mượt, mở rộng từng endpoint.
Có. Nếu mục tiêu là không mang theo quyết định cũ khi yêu cầu thay đổi, nền tảng như AppMaster có thể sinh backend và client từ cùng một model, phù hợp với ý tưởng phát triển dựa trên hợp đồng: một định nghĩa chung, hành vi nhất quán, và ít sự không khớp giữa client và server.


