# Thiết kế API cho giải pháp bàn giao: viết hợp đồng trước, viết code sau

> Đội tích hợp của khách hàng sẽ dùng endpoint của bạn lâu hơn rất nhiều so với thời gian bạn ở lại dự án, nên mỗi tên trường bạn đặt hôm nay là một lời hứa phải giữ.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thiet-ke-api-cho-giai-phap-trien-khai/

Thử hình dung bạn đang ở tuần thứ ba tại site khách hàng. Model chấm rủi ro giao hàng đã chạy và buổi demo đã qua suôn sẻ. Rồi trưởng nhóm ERP bên khách hỏi ba câu: hệ thống của họ gọi vào đâu, nhận về dữ liệu gì, và khi bên bạn thay đổi thì họ được báo bằng cách nào.

Ba câu đó quyết định số phận của giải pháp sau khi bạn rời đi. Model có thể được thay bằng model khác, nhưng các endpoint mà đội ERP đã viết code để gọi vào thì rất khó đổi.

Việc của bạn là thiết kế lớp API đó sao cho đội khách hàng tích hợp được, vận hành được và tự nâng cấp được mà không cần gọi bạn quay lại.

## API là hợp đồng chứ không phải chi tiết kỹ thuật

IBM định nghĩa API là một hợp đồng cho phép phần mềm này truy cập dữ liệu hoặc gọi chức năng mà phần mềm khác cung cấp. Với FDE, chữ "hợp đồng" nên được hiểu theo nghĩa đen. Mỗi trường trong response là một cam kết, và đội bên kia sẽ xây logic nghiệp vụ dựa trên cam kết đó.

Vì thế, câu hỏi đầu tiên cần trả lời là ai được phép gọi API, chưa phải nên dùng REST hay GraphQL. IBM phân loại API theo mức truy cập. API public được mở hoàn toàn ra internet, còn API partner và API private bị giới hạn người dùng.

Một giải pháp chấm rủi ro chỉ phục vụ ERP nội bộ và vài đối tác vận chuyển thì gần như chắc chắn không cần mở public. Xác định đúng điều này ngay từ đầu sẽ giúp bạn tránh được nhiều cuộc họp với đội bảo mật về sau.

## REST hay GraphQL: chọn theo người gọi

Theo IBM, REST là kiến trúc phổ biến nhất cho web API hiện nay, còn SOAP là giao thức message dựa trên XML với những chuẩn chặt chẽ về cấu trúc và cách truyền message. Nếu ERP của khách chỉ nói được SOAP, bạn phải làm việc với SOAP, nhưng với thiết kế mới thì lựa chọn thực tế thường là REST hoặc GraphQL.

Tài liệu chính thức của GraphQL mô tả nó là cách xây API linh hoạt, không cần version, dựa trên một hệ thống kiểu mạnh. Schema định nghĩa hình dạng dữ liệu, và client chỉ yêu cầu đúng những trường mình cần.

Có một chi tiết dễ bị bỏ qua: đặc tả GraphQL cố ý không quy định cách xử lý mạng, phân quyền và phân trang, nên đội của bạn phải tự thiết kế cả ba.

Với một đội tích hợp ERP chỉ cần gọi vài thao tác cố định, REST thường là lựa chọn ít rủi ro hơn khi bàn giao. GraphQL hợp lý hơn khi người dùng chính là nhiều frontend, mỗi frontend cần một lát cắt dữ liệu khác nhau.

## Ví dụ từ đầu đến cuối: API chấm rủi ro lô hàng

Quay lại khách hàng logistics trong tình huống giả định. Bản nháp đầu tiên của đội thường trông như sau: endpoint `POST /getRiskScore`, response trả về `{"shp_id": 8812, "rsk_v2": 0.83, "flg_manual": 1}`. Bản này gọi được, nhưng có ba lỗi sẽ gây hậu quả sau khi bàn giao.

Lỗi thứ nhất là đặt động từ trong URI. Hướng dẫn của Microsoft khuyên đặt URI theo danh từ, tức tài nguyên, thay vì theo động từ, tức thao tác trên tài nguyên.

Lỗi thứ hai là các tên `rsk_v2` và `flg_manual` lấy thẳng từ tên cột trong bảng. Microsoft khuyên không thiết kế API phản chiếu cấu trúc bên trong của database. Lý do là ngày bạn đổi tên cột, mọi client sẽ hỏng theo.

Lỗi thứ ba khó thấy nhất. ERP gửi dữ liệu lô hàng sang, gặp timeout và tự động retry. Nếu endpoint ghi dùng POST, mỗi lần retry có thể tạo thêm một bản ghi.

Microsoft lưu ý PUT bắt buộc phải idempotent, tức là gửi cùng một request nhiều lần vẫn chỉ sửa đúng một tài nguyên với cùng giá trị, còn POST và PATCH thì không được đảm bảo như vậy.

Bản sửa lại được viết thành hợp đồng trước khi viết code. Microsoft gọi cách làm này là contract-first: thiết kế hợp đồng, tức giao diện, rồi mới viết code hiện thực hợp đồng đó. Phần đầu hợp đồng khai báo thao tác ghi:

```yaml
openapi: 3.0.3
info:
title: Shipment Risk API
version: 1.0.0
paths:
/v1/shipments/{shipmentId}:
put:
summary: Tạo hoặc cập nhật lô hàng (idempotent, an toàn khi retry)
```

Phần tiếp theo, nằm cùng trong mục `paths`, mô tả thao tác đọc và hình dạng response mà ERP sẽ nhận:

```yaml
/v1/shipments/{shipmentId}/risk-assessment:
get:
summary: Lấy đánh giá rủi ro mới nhất
responses:
'200':
content:
application/json:
schema:
type: object
required: [shipmentId, riskLevel, score]
properties:
shipmentId: { type: string }
riskLevel: { type: string, enum: [low, medium, high] }
score: { type: number, minimum: 0, maximum: 1 }
reasons: { type: array, items: { type: string } }
```

Hãy để ý những gì đã thay đổi: tài nguyên giờ là `shipments` và `risk-assessment`, đều là danh từ mà người bên ERP hiểu ngay. Ghi dữ liệu dùng PUT theo `shipmentId` do ERP cung cấp, nên retry bao nhiêu lần cũng chỉ có một lô hàng.

Bên trong, một lớp ánh xạ dịch `rsk_v2` thành `score`, nên bạn có thể đổi schema database mà hợp đồng vẫn giữ nguyên.

**Điểm mấu chốt:** File OpenAPI chính là tài liệu bàn giao. Hãy review nó với đội tích hợp trước khi viết dòng code đầu tiên, vì sau khi code xong thì việc sửa tốn kém hơn nhiều.

## Khi nào phải đánh version?

Sáu tháng sau, khách muốn thêm trường `estimatedDelayHours`. Thêm một trường mới thường không phá vỡ client, vì code cũ chỉ bỏ qua trường nó không biết. Ngược lại, đổi `riskLevel` từ chuỗi sang số, hoặc xóa `reasons`, là thay đổi phá vỡ.

Với thay đổi phá vỡ, Microsoft liệt kê bốn chiến lược: version trong URI, trong query string, trong header, hoặc trong media type. Microsoft đánh giá version trong URI như `/v1/` là cách đơn giản. Nhưng họ cũng cảnh báo cách này sẽ trở nên cồng kềnh khi API trải qua nhiều vòng thay đổi và server phải duy trì nhiều version cùng lúc.

GraphQL đi theo hướng khác: tiến hóa hệ thống kiểu theo thời gian mà không đánh version. Trong thực hành, bạn thêm trường mới, giữ trường cũ, và chỉ loại bỏ trường cũ khi chắc chắn không còn client nào dùng.

Dù chọn REST hay GraphQL, quy tắc này cần được ghi vào tài liệu bàn giao để đội khách biết khi nào một trường bị xóa và họ được báo trước bằng cách nào.

## Quy trình làm cho dự án tiếp theo

Bắt đầu bằng việc liệt kê các danh từ nghiệp vụ mà khách hàng hay dùng khi nói chuyện, ví dụ lô hàng, đơn hàng, đánh giá; đó là ứng viên cho tài nguyên. Tiếp theo, với mỗi thao tác ghi, hỏi đội tích hợp xem client của họ có retry khi timeout không.

Nếu có, thiết kế thao tác đó thành PUT trên một ID do client cung cấp. Khi buộc phải giữ POST, cách làm phổ biến là yêu cầu client gửi kèm một idempotency key trong header, để server nhận ra request lặp lại và trả về kết quả cũ thay vì tạo bản ghi mới.

Sau đó viết hợp đồng OpenAPI hoặc schema GraphQL và mang đi review với chính những người sẽ gọi API. Nếu chọn GraphQL, hãy đưa phần phân quyền và phân trang vào cuộc review đó, vì đặc tả không làm thay bạn. Chỉ khi hai bên đã thống nhất, bạn mới viết lớp ánh xạ và code xử lý.

Những lỗi hay gặp nhất đều xuất phát từ những cách làm ẩu cho nhanh: trả thẳng entity của ORM ra response, dùng POST cho mọi thao tác ghi, đổi kiểu dữ liệu một trường mà không tăng version, mở API public trong khi partner là đủ, và chọn GraphQL với giả định nó đã có sẵn phân quyền.

Mỗi lỗi đều tiết kiệm được một buổi chiều lúc viết code, rồi tốn của khách hàng nhiều tuần sau khi bàn giao.

## Thể hiện kỹ năng này khi đi xin việc

Khi đọc JD của một vị trí FDE, hãy để ý các cụm như "integration", "API design" hay "customer-facing technical delivery". Đó là dấu hiệu công việc sẽ dùng đúng kỹ năng này.

Trong CV, thay vì viết "xây REST API", hãy mô tả cụ thể: viết hợp đồng OpenAPI theo kiểu contract-first, review với đội tích hợp của đối tác, và thiết kế endpoint ghi idempotent để client retry an toàn.

Trước buổi phỏng vấn, hãy chuẩn bị sẵn câu trả lời cho tình huống kiểu: khách hàng cần đổi một trường mà nhiều hệ thống đang gọi vào thì bạn làm gì? Câu trả lời thuyết phục nên bắt đầu từ một hợp đồng đã được viết ra từ ngày đầu.

**Thử ngay tuần này:**

- Chọn một endpoint bạn đã viết, rồi viết lại hợp đồng OpenAPI cho nó mà không mở code ra xem. Ghi lại những chỗ hợp đồng đó khác với code đang chạy.
- Rà các endpoint ghi dữ liệu trong dự án hiện tại và đánh dấu những endpoint client có thể retry khi timeout. Với mỗi endpoint, kiểm tra xem gửi cùng một request hai lần có tạo ra bản ghi trùng hay không.
- Tìm trong response API của bạn một trường đang mang tên cột database, rồi viết lớp ánh xạ để đổi nó sang tên nghiệp vụ.

## Nguồn

- [What is an API (application programming interface)? | IBM](https://www.ibm.com/think/topics/api)

- [Learn | GraphQL](https://graphql.org/learn/)

- [GraphQL Best Practices | GraphQL](https://graphql.org/learn/best-practices/)

- [Web API Design Best Practices - Azure Architecture Center | Microsoft Learn](https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design)
