API Concepts Every Software Engineer Should Know: giải thích chi tiết
1. Tổng quan
Gọi API thì dễ — gửi request, đọc JSON, xong. Nhưng thiết kế một API mà team khác, hoặc chính bạn sau 6 tháng, vẫn dùng được mà không phải đọc code để đoán hành vi — đó là chuyện khác hẳn. Bài này tổng hợp lại các khái niệm API mà bất kỳ backend engineer nào cũng nên nắm: từ nền tảng HTTP, các phong cách kiến trúc (REST, GraphQL, gRPC, webhook, WebSocket), đến thiết kế bền vững, bảo mật, độ tin cậy và tài liệu hóa. Đọc xong, bạn sẽ có một checklist cụ thể để tự review API của mình trước khi public cho người khác dùng.
2. Nền tảng HTTP: method, status code, request/response
Trước khi nói REST hay GraphQL, bạn phải dùng đúng ngữ nghĩa HTTP cơ bản. Đây là chỗ nhiều API “trông giống REST” nhưng thực ra sai be bét.
@RestController
@RequestMapping("/api/orders")
class OrderController {
private final OrderService orderService;
OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
ResponseEntity<Order> getOrder(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
Order created = orderService.create(request);
return ResponseEntity.created(URI.create("/api/orders/" + created.id())).body(created);
}
@PutMapping("/{id}")
ResponseEntity<Order> replaceOrder(@PathVariable Long id, @RequestBody Order order) {
return ResponseEntity.ok(orderService.replace(id, order));
}
@DeleteMapping("/{id}")
ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
orderService.cancel(id);
return ResponseEntity.noContent().build();
}
}
Mấy điểm dễ sai:
GETphải safe (không đổi state) và idempotent. Nếu endpointGETcủa bạn tăng counter hay ghi log quan trọng, bạn đang vi phạm hợp đồng ngầm với client (và với proxy/CDN cache).PUTthay thế toàn bộ resource,PATCHchỉ cập nhật phần thay đổi. DùngPUTđể cập nhật một field là sai ngữ nghĩa, dù code vẫn chạy.- Status code phải phản ánh đúng ý nghĩa:
201 Createdkhi tạo mới (kèm headerLocation),204 No Contentkhi xóa thành công không trả body,409 Conflictkhi xung đột state — không phải cứ lỗi là400hay500.
Lưu ý: Trả
200 OKcho mọi response kèm field"success": falsetrong body là anti-pattern phổ biến. Client, load balancer, APM tool đều dựa vào status code để quyết định retry hay alert — che giấu lỗi trong body làm hỏng toàn bộ chuỗi đó.
3. Chọn phong cách API: REST, GraphQL, gRPC, webhook, WebSocket
Mỗi kiểu kiến trúc giải quyết một bài toán khác nhau. Chọn sai không phải lỗi kỹ thuật — nó là lỗi hiểu sai use case.
3.1. REST — mặc định cho CRUD qua HTTP
Phù hợp khi resource rõ ràng (Order, Customer, Invoice) và client đa dạng (web, mobile, third-party). Ví dụ ở mục 2 chính là REST chuẩn.
3.2. GraphQL — khi client cần linh hoạt field
type Order {
id: ID!
customer: Customer!
total: Float!
items: [OrderItem!]!
}
type Query {
order(id: ID!): Order
}
@Controller
class OrderGraphQlController {
private final OrderService orderService;
OrderGraphQlController(OrderService orderService) {
this.orderService = orderService;
}
@QueryMapping
Order order(@Argument Long id) {
return orderService.findById(id).orElse(null);
}
}
Client tự chọn field cần lấy ({ order(id: 1) { total } } thay vì phải gọi 3 REST endpoint). Đổi lại, bạn mất cache HTTP theo URL, và phải tự lo N+1 query (dùng DataLoader). GraphQL hợp với app có UI phức tạp, nhiều client khác nhau cần dữ liệu khác nhau — không hợp với API đơn giản, ít client.
3.3. gRPC — khi cần hiệu năng cao giữa service nội bộ
service PaymentService {
rpc Charge (ChargeRequest) returns (ChargeResponse);
}
message ChargeRequest {
string order_id = 1;
int64 amount_cents = 2;
}
gRPC dùng HTTP/2 + Protobuf (binary, nhỏ gọn hơn JSON), hỗ trợ streaming hai chiều. Rất hợp cho service-to-service trong microservices, nhưng khó debug bằng tay (không đọc được bằng curl thường) và không hợp với public API cho browser.
3.4. Webhook — khi bên nhận cần được thông báo chủ động
@PostMapping("/webhooks/payment-status")
ResponseEntity<Void> handlePaymentWebhook(@RequestBody PaymentStatusEvent event,
@RequestHeader("X-Signature") String signature) {
if (!signatureValidator.isValid(event, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
paymentEventHandler.handle(event);
return ResponseEntity.ok().build();
}
Webhook đảo ngược chiều gọi: server của PaymentService gọi ngược vào server của bạn khi có event. Luôn verify signature (HMAC) để tránh fake request, và luôn trả 200 nhanh rồi xử lý async — bên gửi thường retry nếu timeout hoặc nhận lỗi.
3.5. WebSocket — khi cần real-time hai chiều
Dùng cho chat, live tracking, dashboard cập nhật liên tục. Không hợp cho request-response đơn giản vì overhead giữ connection liên tục tốn resource hơn REST nhiều.
4. Thiết kế bền vững: naming, pagination, versioning, error response
4.1. Naming nhất quán
Dùng danh từ số nhiều cho collection (/orders, không /order hay /getOrders), danh từ lồng nhau thể hiện quan hệ (/customers/{id}/orders). Tránh trộn camelCase và snake_case trong cùng một API.
4.2. Pagination
@GetMapping
Page<Order> listOrders(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return orderService.findAll(PageRequest.of(page, size));
}
GET /api/orders?page=1&size=20
Offset-based (page/size) dễ hiểu nhưng chậm với bảng lớn và dễ trả trùng/thiếu record khi có insert/delete giữa lúc phân trang. Cursor-based (?after=order_1234) ổn định hơn với dataset lớn, đổi lại phức tạp hơn để implement “jump to page 10”.
4.3. Versioning
@RestController
@RequestMapping("/api/v2/orders")
class OrderControllerV2 { /* ... */ }
Versioning qua URI path dễ đọc, dễ route bằng gateway. Versioning qua header (Accept: application/vnd.myapp.v2+json) sạch hơn về URL nhưng khó debug bằng browser. Chọn cách nào không quan trọng bằng việc có chiến lược deprecation rõ ràng: thông báo trước, giữ version cũ chạy đủ lâu, trả header Deprecation và Sunset.
4.4. Error response chuẩn hóa
@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<ProblemDetail> handleNotFound(OrderNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setProperty("orderId", ex.getOrderId());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problem);
}
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Order 99 not found",
"orderId": 99
}
ProblemDetail (Spring Boot 3.x hỗ trợ sẵn, theo RFC 7807) cho ra format lỗi đồng nhất toàn hệ thống — client chỉ cần parse một schema duy nhất, không phải đoán field nào chứa message lỗi.
5. Bảo mật: API key, OAuth2, JWT, scope
5.1. API key — đơn giản, hợp cho service-to-service tin cậy thấp rủi ro thấp
@Component
class ApiKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String apiKey = req.getHeader("X-Api-Key");
if (apiKey == null || !apiKeyRepository.isValid(apiKey)) {
res.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
chain.doFilter(req, res);
}
}
API key không phân biệt được ai đang gọi thay mặt ai — nó chỉ xác thực app, không xác thực user. Đừng dùng nó để bảo vệ dữ liệu cá nhân của người dùng cuối.
5.2. OAuth2 + JWT — khi cần phân quyền theo scope
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.nonladev.com
@PreAuthorize("hasAuthority('SCOPE_orders:write')")
@PostMapping
ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
return ResponseEntity.ok(orderService.create(request));
}
JWT mang theo claim (user id, scope, exp) tự verify được bằng public key, không cần hỏi lại auth server mỗi request — đây là lý do nó scale tốt hơn session truyền thống. Nhưng JWT không revoke được dễ dàng trước khi hết hạn: nếu user bị khóa tài khoản, token cũ vẫn valid cho tới khi exp. Giải pháp thường dùng: để exp ngắn (5–15 phút) kèm refresh token, hoặc check blacklist ở layer gateway.
Lưu ý: Scope chỉ nói “token này được phép làm gì”, không thay thế việc kiểm tra business rule (ví dụ: user A có
orders:writenhưng không có quyền sửa order của user B). Luôn kiểm tra ownership ở service layer.
6. Độ tin cậy: timeout, retry, idempotency, rate limit, cache
6.1. Timeout và retry
@Bean
RestClient paymentClient() {
return RestClient.builder()
.baseUrl("https://payment.internal")
.requestFactory(ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofSeconds(2))
.withReadTimeout(Duration.ofSeconds(3))
.buildRequestFactory())
.build();
}
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
@Retry(name = "paymentService", fallbackMethod = "chargeFallback")
Charge charge(ChargeRequest request) {
return paymentClient.post().uri("/charge").body(request).retrieve().body(Charge.class);
}
Không set timeout = thread bị treo vô hạn khi downstream chết, kéo sập cả service của bạn theo domino effect. Retry mà không cẩn thận lại gây ra storm request làm downstream sập nặng hơn — luôn kết hợp retry với exponential backoff và circuit breaker.
6.2. Idempotency key cho operation ghi tiền
@PostMapping("/charges")
ResponseEntity<Charge> charge(@RequestHeader("Idempotency-Key") String key,
@RequestBody ChargeRequest request) {
return idempotencyStore.findByKey(key)
.map(existing -> ResponseEntity.ok(existing))
.orElseGet(() -> {
Charge charge = paymentService.charge(request);
idempotencyStore.save(key, charge);
return ResponseEntity.ok(charge);
});
}
Client gửi lại cùng request (do timeout, network glitch) với cùng Idempotency-Key sẽ nhận lại kết quả cũ, không bị charge hai lần. Đây là concept quan trọng nhất với mọi API có side-effect tài chính.
@Test
void shouldReturnSameChargeForDuplicateIdempotencyKey() {
ChargeRequest request = new ChargeRequest("order-1", 10_000L);
Charge first = client.charge("idem-key-1", request);
Charge second = client.charge("idem-key-1", request);
assertThat(second).isEqualTo(first);
assertThat(paymentGateway.chargeCallCount()).isEqualTo(1);
}
6.3. Rate limit và cache
@Bean
Bucket4jConfiguration rateLimitConfig() {
return Bucket4jConfiguration.builder()
.capacity(100)
.refillGreedy(100, Duration.ofMinutes(1))
.build();
}
@Cacheable(value = "orders", key = "#id")
Order findById(Long id) {
return orderRepository.findById(id).orElseThrow();
}
Rate limit bảo vệ hệ thống khỏi bị quá tải bởi một client lỗi (vô tình hay cố ý). Cache giảm tải cho downstream nhưng phải cân nhắc TTL và cache invalidation — cache stale cho dữ liệu order đang xử lý có thể gây hiểu sai trạng thái nghiêm trọng hơn là không cache.
7. Documentation, observability và contract testing
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
@Operation(summary = "Tạo order mới", responses = {
@ApiResponse(responseCode = "201", description = "Order được tạo thành công")
})
@PostMapping
ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) { /* ... */ }
OpenAPI spec tự sinh từ annotation giúp spec không bị “lệch” so với code thật — vấn đề kinh điển khi doc viết tay rồi code đổi mà quên update.
Contract testing (Spring Cloud Contract, Pact) đảm bảo consumer và provider không phá hợp đồng khi deploy độc lập:
@Test
void providerShouldHonorOrderContract() {
Order order = orderService.findById(1L).orElseThrow();
assertThat(order.id()).isEqualTo(1L);
assertThat(order.status()).isIn("PENDING", "PAID", "CANCELLED");
}
Kết hợp với observability (structured logging, trace ID xuyên service qua header traceparent, metrics latency/error rate theo endpoint), team vận hành API mới có đủ thông tin để debug production thay vì đoán mò qua log rời rạc.
8. Kết luận
API design không nằm ở việc chọn đúng framework — nó nằm ở hàng loạt quyết định nhỏ: method nào, status code nào, pagination kiểu gì, ai được gọi với scope nào, retry bao nhiêu lần thì dừng. Từng quyết định riêng lẻ trông đơn giản, nhưng gộp lại chúng quyết định API của bạn dễ dùng hay là nguồn đau đầu cho mọi team tích hợp. Nắm chắc các khái niệm ở trên — HTTP semantics, lựa chọn kiến trúc đúng use case, bảo mật theo tầng, reliability pattern, và tài liệu đồng bộ với code — là nền tảng để bạn thiết kế API mà người khác thực sự tin tưởng dùng lâu dài.