APIAPIs
Thuộc bộ kiến thức Backend Roadmap.
Tổng quan
API (Application Programming Interface) là một bản hợp đồng (contract) cho phép một phần mềm giao tiếp với phần mềm khác. Nó định nghĩa bạn có thể yêu cầu điều gì, yêu cầu như thế nào, và nhận lại được gì — trong khi giấu đi phần implementation phía sau ranh giới đó. Với backend engineer, “API” gần như luôn có nghĩa là web API: một tập các operation được expose qua mạng (thường là HTTP) để browser, mobile app, các service khác và third-party developer có thể sử dụng khả năng cùng dữ liệu của hệ thống bạn.
Một API tốt là một sản phẩm, không phải thứ nghĩ sau. Nó là bề mặt mà consumer thực sự phụ thuộc vào, và một khi đã public thì rất khó thay đổi. Các họ API phổ biến bạn sẽ gặp là REST, GraphQL, gRPC, và SOAP (legacy). Mỗi loại có những đánh đổi khác nhau về transport, định dạng dữ liệu, mức độ coupling, streaming và tooling. Chọn đúng — và thiết kế contract một cách có chủ đích — là một trong những quyết định có đòn bẩy cao nhất trong công việc backend.
API-first design
API-first nghĩa là bạn thiết kế và thống nhất về API contract trước khi viết code implementation. Contract (một tài liệu OpenAPI, một GraphQL schema, một file .proto) trở thành single source of truth để các team frontend, backend và QA làm việc song song. Lợi ích:
- Phát triển song song — client có thể generate stub và mock server từ contract trong khi backend còn đang xây dựng.
- Tính nhất quán — một bộ từ vựng và quy ước chung xuyên suốt các team và service.
- Tooling — code generation, mock server, contract test và tài liệu đều sinh ra từ spec.
- Ít bất ngờ gây breaking — contract được review và version hóa như mọi tài sản quan trọng khác.
Phần lớn web API hiện đại trao đổi bằng JSON vì nó dễ đọc với con người, phổ biến khắp nơi và native với JavaScript. Các định dạng binary như Protocol Buffers đánh đổi khả năng đọc để lấy sự gọn nhẹ và tốc độ.
Kiến thức nền tảng
Vòng đời request/response trên HTTP
Gần như mọi web API đều chạy trên HTTP. Một request mang theo một method (verb), một URL/path, các header (metadata như Content-Type, Authorization, Accept), và một body tùy chọn. Response mang theo một status code, các header, và một body tùy chọn.
POST /v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer <token>
Accept: application/json
{ "productId": "sku_123", "quantity": 2 }
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/orders/ord_789
{ "id": "ord_789", "status": "pending", "total": 49.98 }
HTTP methods
| Method | Mục đích | Safe | Idempotent |
|---|---|---|---|
GET | Đọc một resource | Có | Có |
POST | Tạo resource / action không idempotent | Không | Không |
PUT | Thay thế toàn bộ resource | Không | Có |
PATCH | Cập nhật một phần resource | Không | Không* |
DELETE | Xóa một resource | Không | Có |
HEAD | Giống GET nhưng chỉ trả header | Có | Có |
OPTIONS | Khám phá method được phép (dùng bởi CORS preflight) | Có | Có |
*PATCH có thể được làm cho idempotent tùy vào ngữ nghĩa của patch.
Safe = không thay đổi state phía server. Idempotent = gọi cùng một request N lần cho hiệu ứng giống hệt như gọi một lần.
HTTP status codes
| Nhóm | Ý nghĩa | Ví dụ thường gặp |
|---|---|---|
2xx | Thành công | 200 OK, 201 Created, 202 Accepted, 204 No Content |
3xx | Redirect | 301 Moved Permanently, 304 Not Modified |
4xx | Lỗi client | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests |
5xx | Lỗi server | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout |
Dùng status code cho chính xác: 401 nghĩa là “chưa authenticate”, 403 nghĩa là “đã authenticate nhưng không được phép”, 404 giấu đi sự tồn tại, 409 báo hiệu xung đột (ví dụ version mismatch), 422 nghĩa là payload đã hiểu được nhưng không hợp lệ về mặt ngữ nghĩa.
Khái niệm chính
REST
REST (Representational State Transfer) là một architectural style được Roy Fielding định nghĩa trong luận án năm 2000 của ông. Nó không phải là một protocol hay một standard — nó là một tập các ràng buộc (constraint) mà khi tuân theo sẽ tạo ra các hệ thống có khả năng mở rộng, dễ tiến hóa và loosely-coupled. Các constraint cốt lõi gồm: client–server, statelessness, cacheability, uniform interface, layered system, và tùy chọn code-on-demand.
Resource và representation
REST mô hình hóa domain thành các resource — những danh từ được định danh bằng URI. Một resource có một hoặc nhiều representation (thường là JSON). Thiết kế URL xoay quanh resource, không phải action:
GET /v1/users # liệt kê users
POST /v1/users # tạo một user
GET /v1/users/42 # lấy user 42
PUT /v1/users/42 # thay thế user 42
PATCH /v1/users/42 # cập nhật một phần user 42
DELETE /v1/users/42 # xóa user 42
GET /v1/users/42/orders # liệt kê order thuộc về user 42
Dùng danh từ số nhiều cho collection, lồng sub-resource ở mức nông, và tránh verb trong path (/getUser, /createOrder là anti-pattern).
Statelessness
Mỗi request phải chứa mọi thứ server cần để xử lý nó — server không giữ session state của client giữa các request. Thông tin authentication đi kèm mỗi request (ví dụ một Bearer token). Chính statelessness cho phép bất kỳ server instance nào phía sau load balancer đều có thể xử lý bất kỳ request nào, tạo điều kiện cho horizontal scaling.
HATEOAS và Richardson Maturity Model
HATEOAS (Hypermedia As The Engine Of Application State) là constraint bị bỏ qua nhiều nhất của REST: response bao gồm các link cho client biết nó có thể làm gì tiếp theo, nhờ đó client điều hướng API bằng cách đi theo link thay vì hard-code URL.
{
"id": "ord_789",
"status": "pending",
"total": 49.98,
"_links": {
"self": { "href": "/v1/orders/ord_789" },
"cancel": { "href": "/v1/orders/ord_789/cancel", "method": "POST" },
"pay": { "href": "/v1/orders/ord_789/payment", "method": "POST" }
}
}
Richardson Maturity Model chấm điểm mức độ “RESTful” của một API:
| Level | Tên | Mô tả |
|---|---|---|
| 0 | The Swamp of POX | Một URI, một verb (thường là POST) — RPC trên HTTP |
| 1 | Resources | Nhiều URI (resource), vẫn một verb |
| 2 | HTTP Verbs | Dùng đúng method + status code (nơi phần lớn API “REST” đang ở) |
| 3 | Hypermedia | HATEOAS — link điều khiển các chuyển đổi trạng thái |
Versioning
API tiến hóa; versioning bảo vệ consumer hiện tại khỏi các breaking change. Các chiến lược phổ biến:
- URI path —
/v1/users,/v2/users(phổ biến nhất, dễ thấy nhất). - Header —
Accept: application/vnd.example.v2+json(URL sạch hơn, khó test trên browser hơn). - Query param —
/users?version=2(đơn giản nhưng làm rối query).
Chỉ tăng version cho các thay đổi breaking; các thay đổi mang tính bổ sung (field optional mới, endpoint mới) nên backward compatible.
Pagination, filtering, sorting
Đừng bao giờ trả về một list không giới hạn. Các kiểu pagination phổ biến:
- Offset/limit —
GET /users?limit=20&offset=40. Đơn giản, nhưng chậm và không nhất quán trên dataset lớn, hay thay đổi. - Cursor/keyset —
GET /users?limit=20&cursor=eyJpZCI6NDJ9. Ổn định và nhanh cho infinite scroll; cursor mã hóa vị trí.
{
"data": [ { "id": 41 }, { "id": 42 } ],
"pagination": {
"limit": 20,
"nextCursor": "eyJpZCI6NjJ9",
"hasMore": true
}
}
Filtering và sorting qua query param: GET /orders?status=paid&sort=-createdAt&minTotal=100.
Idempotency
Với các operation không idempotent (như POST thanh toán), hãy để client gửi một idempotency key để request bị retry không tạo ra bản ghi trùng lặp:
POST /v1/payments
Idempotency-Key: 5f3a9c2e-...
{ "amount": 4998, "currency": "usd" }
Server lưu key cùng với kết quả; một request lặp lại với cùng key sẽ trả về response gốc thay vì charge hai lần. Đây là cách Stripe và nhiều bên khác làm cho retry trở nên an toàn.
Ví dụ JSON đầy đủ
GET /v1/products?category=books&limit=2 HTTP/1.1
Accept: application/json
{
"data": [
{
"id": "prd_1",
"name": "Clean Architecture",
"price": { "amount": 3499, "currency": "usd" },
"inStock": true
},
{
"id": "prd_2",
"name": "Designing Data-Intensive Applications",
"price": { "amount": 5499, "currency": "usd" },
"inStock": false
}
],
"pagination": { "limit": 2, "nextCursor": "eyJpZCI6InByZF8yIn0", "hasMore": true }
}
GraphQL
GraphQL là một query language và runtime cho API, được tạo tại Facebook và hiện thuộc GraphQL Foundation. Thay vì nhiều endpoint, nó expose một endpoint duy nhất và một schema có kiểu chặt chẽ (strongly-typed); client yêu cầu chính xác các field cần thiết trong một round trip.
Schema
Schema chính là contract, được viết bằng SDL (Schema Definition Language):
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
}
enum OrderStatus { PENDING PAID CANCELLED }
type Query {
user(id: ID!): User
users(limit: Int = 20): [User!]!
}
type Mutation {
createOrder(userId: ID!, productId: ID!): Order!
}
type Subscription {
orderStatusChanged(orderId: ID!): Order!
}
- Query — các operation đọc.
- Mutation — các operation ghi.
- Subscription — một stream cập nhật tồn tại lâu (thường qua WebSocket) cho dữ liệu real-time.
Query và resolver
Một query yêu cầu một hình dạng chính xác; response phản chiếu đúng hình dạng đó:
query GetUserWithOrders {
user(id: "42") {
name
email
orders {
id
total
status
}
}
}
{
"data": {
"user": {
"name": "Ada",
"email": "ada@example.com",
"orders": [
{ "id": "ord_1", "total": 49.98, "status": "PAID" }
]
}
}
}
Mỗi field được backing bởi một resolver — một hàm trả về dữ liệu cho field đó. Các resolver kết hợp với nhau: field orders trên User được resolve bởi một hàm riêng, hàm này nhận User cha làm tham số.
Over-fetching, under-fetching và N+1
Điểm bán hàng chính của GraphQL là loại bỏ over-fetching (lấy về những field bạn không cần) và under-fetching (phải gọi nhiều REST call để ghép nên một màn hình). Client chỉ định chính xác cây dữ liệu nó muốn.
Cạm bẫy kinh điển của GraphQL là N+1 problem: resolve một list N user rồi resolve orders cho từng user sẽ bắn 1 query cho user cộng thêm N query cho order của họ. Cách khắc phục chuẩn là DataLoader — nó gom (batch) N lượt tra cứu theo từng item thành một query gom chung và cache trong phạm vi một request.
// Không batch: 1 + N query
// Với DataLoader: 1 (users) + 1 (orders đã gom) query
const orderLoader = new DataLoader(async (userIds) => {
const rows = await db.orders.whereIn('userId', userIds);
return userIds.map((id) => rows.filter((r) => r.userId === id));
});
Đánh đổi REST vs GraphQL
GraphQL tỏa sáng với các UI hướng client, nặng về tổng hợp dữ liệu (mobile app, dashboard). Chi phí: HTTP caching khó hơn (mọi thứ là POST tới một endpoint), độ phức tạp của query phải bị giới hạn để tránh lạm dụng, và server phức tạp hơn. REST giữ cho CRUD đơn giản luôn đơn giản và tận dụng HTTP caching miễn phí.
gRPC
gRPC là một RPC framework hiệu năng cao từ Google, dùng Protocol Buffers (protobuf) cho schema/serialization và HTTP/2 cho transport. Bạn định nghĩa service và message trong một file .proto, rồi generate code client và server có kiểu chặt chẽ ở nhiều ngôn ngữ.
syntax = "proto3";
package shop.v1;
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc WatchOrders(WatchRequest) returns (stream Order); // server streaming
}
message GetOrderRequest { string id = 1; }
message Order {
string id = 1;
double total = 2;
Status status = 3;
enum Status { PENDING = 0; PAID = 1; CANCELLED = 2; }
}
Các đặc tính chính:
- Binary protobuf gọn hơn và (de)serialize nhanh hơn nhiều so với JSON.
- HTTP/2 mang lại multiplexing, header compression, và streaming native: unary, server-streaming, client-streaming và bidirectional-streaming.
- Code generation cho bạn stub type-safe ở cả hai đầu.
Khi nào dùng gRPC: giao tiếp service-to-service nội bộ trong backend microservices/đa ngôn ngữ, các đường (path) yêu cầu latency thấp và throughput cao, và các workload streaming. Khi nào không dùng: API public hướng browser (browser không nói được raw gRPC nếu không có gRPC-Web và một proxy), hoặc khi payload dễ đọc với con người và khả năng curl dễ dàng là quan trọng.
SOAP (legacy)
SOAP (Simple Object Access Protocol) là một protocol nhắn tin cũ hơn, dựa trên XML. Contract được mô tả bằng WSDL, message là các envelope XML, và nó mang theo một tập nặng các standard WS-* (WS-Security, WS-ReliableMessaging, v.v.). Nó transport-agnostic (thường là HTTP, đôi khi message queue) và strongly typed.
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<getOrder xmlns="http://example.com/shop">
<id>ord_789</id>
</getOrder>
</soap:Body>
</soap:Envelope>
Bạn sẽ chủ yếu gặp SOAP trong các hệ thống enterprise, ngân hàng và legacy của chính phủ. Với công việc mới, hãy ưu tiên REST, GraphQL hoặc gRPC — nhưng biết rằng SOAP tồn tại (và tại sao tính hình thức của nó từng có giá trị) vẫn hữu ích.
Tài liệu API và contract
OpenAPI / Swagger
OpenAPI (trước đây là Swagger) là specification chuẩn, không phụ thuộc ngôn ngữ, để mô tả các REST API. Một tài liệu YAML/JSON duy nhất mô tả path, method, parameter, schema request/response, auth và ví dụ. Từ nó bạn có thể generate tài liệu tương tác (Swagger UI), client SDK, server stub và contract test.
openapi: 3.0.3
info:
title: Shop API
version: 1.0.0
paths:
/v1/orders/{id}:
get:
summary: Get an order by ID
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
'200':
description: The order
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Not found
components:
schemas:
Order:
type: object
properties:
id: { type: string }
total: { type: number, format: float }
status: { type: string, enum: [pending, paid, cancelled] }
required: [id, total, status]
Với GraphQL, chính schema là contract và cho phép tooling dựa trên introspection (GraphiQL, Apollo Studio). Với gRPC, file .proto là contract. Ở cả ba, contract có thể đọc được bằng máy — chứ không phải văn bản mô tả — mới là source of truth.
Các mối quan tâm xuyên suốt (cross-cutting)
Authentication và authorization
Mọi API không public đều cần biết ai đang gọi (authentication) và họ được phép làm gì (authorization). Các cơ chế phổ biến: API key, OAuth 2.0 / OpenID Connect, và JWT bearer token. Xem Authentication & Authorization và Web Security để đi sâu. Luôn truyền credential qua TLS và không bao giờ đặt trong URL.
CORS
CORS (Cross-Origin Resource Sharing) là một cơ chế bảo mật của browser kiểm soát origin nào được phép gọi API của bạn từ JavaScript. Browser gửi một request preflight OPTIONS cho các request không đơn giản; server của bạn phải phản hồi với đúng các header Access-Control-Allow-*.
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
CORS chỉ ảnh hưởng tới browser; nó không thay thế cho authorization phía server.
Rate limiting và throttling
Bảo vệ API khỏi lạm dụng và quá tải bằng cách giới hạn số request một client có thể thực hiện trong một khoảng thời gian (token-bucket, sliding-window, v.v.). Truyền đạt giới hạn bằng các header chuẩn và trả về 429 Too Many Requests kèm Retry-After khi vượt ngưỡng:
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 1000
RateLimit-Remaining: 0
RateLimit-Reset: 42
Retry-After: 42
Định dạng lỗi (RFC 7807 / RFC 9457)
Trả về lỗi theo một hình dạng nhất quán, đọc được bằng máy. RFC 7807 “Problem Details for HTTP APIs” (được cập nhật bởi RFC 9457) định nghĩa application/problem+json:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://example.com/probs/validation",
"title": "Validation failed",
"status": 422,
"detail": "quantity must be greater than 0",
"instance": "/v1/orders",
"errors": [
{ "field": "quantity", "message": "must be > 0" }
]
}
Một error contract ổn định cho phép client lập trình dựa trên lỗi thay vì phải parse văn bản mô tả.
Content negotiation
Client khai báo thứ nó có thể chấp nhận và gửi đi; server phản hồi tương ứng. Được điều khiển bằng header:
Accept: application/json— client muốn nhận lại JSON.Content-Type: application/json— body của request là JSON.Accept-Language,Accept-Encoding— thương lượng ngôn ngữ và nén (ví dụgzip,br).
Nếu server không thể đáp ứng header Accept thì nó trả về 406 Not Acceptable.
Real-time
Mô hình request-response của REST/GraphQL không phù hợp với nhu cầu real-time dựa trên push. Với các nhu cầu đó, hãy dùng WebSocket, SSE, GraphQL subscription, hoặc gRPC streaming — xem Real-time Communication.
So sánh: REST vs GraphQL vs gRPC vs SOAP
| Tiêu chí | REST | GraphQL | gRPC | SOAP |
|---|---|---|---|---|
| Transport | HTTP/1.1, HTTP/2 | HTTP (một endpoint) | HTTP/2 | HTTP, SMTP, MQ |
| Định dạng payload | JSON (thường), bất kỳ | JSON | Protobuf (binary) | XML |
| Contract | OpenAPI (tùy chọn) | Schema (SDL, bắt buộc) | .proto (bắt buộc) | WSDL (bắt buộc) |
| Coupling | Lỏng | Trung bình | Chặt (stub generated) | Chặt |
| Streaming | Hạn chế (SSE/chunked) | Subscriptions | First-class (4 chế độ) | Không (native) |
| Caching | Tuyệt vời (HTTP) | Khó (client-side) | Hạn chế | Hạn chế |
| Thân thiện browser | Có | Có | Không (cần gRPC-Web) | Có (dài dòng) |
| Use case tốt nhất | Web API public/CRUD | UI hướng client, tổng hợp dữ liệu | Microservices nội bộ, latency thấp | Legacy/enterprise, WS-* |
| Tooling | Rất lớn, trưởng thành | Mạnh (Apollo, v.v.) | Mạnh (codegen) | Trưởng thành nhưng nặng |
Best Practices
- Thiết kế contract trước. Thống nhất về OpenAPI/schema/
.prototrước khi code; coi nó là source of truth và review như code. - Dùng danh từ và ngữ nghĩa HTTP đúng chuẩn. URL hướng resource, method đúng, và status code chính xác. Không đặt verb trong REST path.
- Version hóa ngay từ đầu và chỉ break trong một major version mới; giữ các thay đổi bổ sung backward compatible.
- Luôn paginate collection; ưu tiên cursor pagination cho dataset lớn, hay thay đổi.
- Chuẩn hóa lỗi (RFC 7807/9457) và không bao giờ để lộ stack trace hay chi tiết nội bộ cho client.
- Làm cho các thao tác ghi an toàn khi retry bằng idempotency key cho
POST; dùng ngữ nghĩaPUT/DELETEở nơi phù hợp. - Secure by default: TLS khắp nơi, authenticate mọi request, authorize theo từng resource, validate mọi input, và đặt CORS chặt chẽ.
- Rate-limit và throttle với header rõ ràng và response
429; bảo vệ các operation tốn kém. - Giới hạn chi phí query trong GraphQL (giới hạn depth/complexity, persisted query) và giải quyết N+1 bằng DataLoader.
- Ưu tiên gRPC cho các đường service-to-service nội bộ nóng; expose REST/GraphQL ở biên (edge) cho client.
- Tài liệu hóa kèm ví dụ, generate tài liệu tương tác, và giữ tài liệu đồng bộ bằng cách generate từ contract.
- Nhất quán trên toàn bộ bề mặt API: naming, casing (chọn
snake_casehoặccamelCasevà giữ nguyên), định dạng ngày (ISO 8601), và hình dạng envelope.
Tài liệu tham khảo
- Roy Fielding — Architectural Styles and the Design of Network-based Software Architectures (luận án REST)
- MDN — HTTP overview and methods
- OpenAPI Specification
- GraphQL — tài liệu chính thức
- gRPC — tài liệu chính thức
- Protocol Buffers documentation
- RFC 9457 — Problem Details for HTTP APIs
- Richardson Maturity Model (Martin Fowler)
Part of the Backend Roadmap knowledge base.
Overview
An API (Application Programming Interface) is a contract that lets one piece of software talk to another. It defines what you can ask for, how you ask, and what you get back — while hiding the implementation behind the boundary. For backend engineers, “API” almost always means a web API: a set of operations exposed over the network (usually HTTP) so that browsers, mobile apps, other services, and third-party developers can consume your system’s capabilities and data.
A good API is a product, not an afterthought. It is the surface your consumers actually depend on, and once it is public it becomes very hard to change. The dominant families you will encounter are REST, GraphQL, gRPC, and the legacy SOAP. Each makes different trade-offs around transport, data format, coupling, streaming, and tooling. Choosing well — and designing the contract deliberately — is one of the highest-leverage decisions in backend work.
API-first design
API-first means you design and agree on the API contract before writing implementation code. The contract (an OpenAPI document, a GraphQL schema, a .proto file) becomes the single source of truth that frontend, backend, and QA teams work against in parallel. Benefits:
- Parallel development — clients can generate stubs and mock servers from the contract while the backend is still being built.
- Consistency — one shared vocabulary and set of conventions across teams and services.
- Tooling — code generation, mock servers, contract tests, and documentation all flow from the spec.
- Fewer breaking surprises — the contract is reviewed and versioned like any other critical asset.
Most modern web APIs exchange JSON because it is human-readable, ubiquitous, and native to JavaScript. Binary formats like Protocol Buffers trade readability for compactness and speed.
Fundamentals
The request/response cycle over HTTP
Nearly all web APIs ride on HTTP. A request carries a method (verb), a URL/path, headers (metadata such as Content-Type, Authorization, Accept), and an optional body. The response carries a status code, headers, and an optional body.
POST /v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer <token>
Accept: application/json
{ "productId": "sku_123", "quantity": 2 }
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/orders/ord_789
{ "id": "ord_789", "status": "pending", "total": 49.98 }
HTTP methods
| Method | Purpose | Safe | Idempotent |
|---|---|---|---|
GET | Read a resource | Yes | Yes |
POST | Create a resource / non-idempotent action | No | No |
PUT | Replace a resource fully | No | Yes |
PATCH | Update a resource partially | No | No* |
DELETE | Remove a resource | No | Yes |
HEAD | Like GET but headers only | Yes | Yes |
OPTIONS | Discover allowed methods (used by CORS preflight) | Yes | Yes |
*PATCH can be made idempotent depending on the patch semantics.
Safe = does not modify server state. Idempotent = making the same call N times has the same effect as making it once.
HTTP status codes
| Range | Meaning | Common examples |
|---|---|---|
2xx | Success | 200 OK, 201 Created, 202 Accepted, 204 No Content |
3xx | Redirection | 301 Moved Permanently, 304 Not Modified |
4xx | Client error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests |
5xx | Server error | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout |
Use codes precisely: 401 means “not authenticated”, 403 means “authenticated but not allowed”, 404 hides existence, 409 signals a conflict (e.g., version mismatch), 422 means the payload was understood but semantically invalid.
Key Concepts
REST
REST (Representational State Transfer) is an architectural style defined by Roy Fielding in his 2000 dissertation. It is not a protocol or a standard — it is a set of constraints that, when followed, yield scalable, evolvable, loosely-coupled systems. The core constraints are: client–server, statelessness, cacheability, uniform interface, layered system, and optionally code-on-demand.
Resources and representations
REST models the domain as resources — nouns identified by URIs. A resource has one or more representations (typically JSON). Design URLs around resources, not actions:
GET /v1/users # list users
POST /v1/users # create a user
GET /v1/users/42 # get user 42
PUT /v1/users/42 # replace user 42
PATCH /v1/users/42 # partially update user 42
DELETE /v1/users/42 # delete user 42
GET /v1/users/42/orders # list orders belonging to user 42
Use plural nouns for collections, nest sub-resources shallowly, and avoid verbs in paths (/getUser, /createOrder are anti-patterns).
Statelessness
Each request must contain everything the server needs to process it — the server keeps no client session state between requests. Authentication travels on every request (e.g., a Bearer token). Statelessness is what allows any server instance behind a load balancer to handle any request, enabling horizontal scaling.
HATEOAS and the Richardson Maturity Model
HATEOAS (Hypermedia As The Engine Of Application State) is REST’s most-ignored constraint: responses include links telling the client what it can do next, so clients navigate the API by following links rather than hard-coding URLs.
{
"id": "ord_789",
"status": "pending",
"total": 49.98,
"_links": {
"self": { "href": "/v1/orders/ord_789" },
"cancel": { "href": "/v1/orders/ord_789/cancel", "method": "POST" },
"pay": { "href": "/v1/orders/ord_789/payment", "method": "POST" }
}
}
The Richardson Maturity Model grades how “RESTful” an API is:
| Level | Name | Description |
|---|---|---|
| 0 | The Swamp of POX | Single URI, single verb (usually POST) — RPC over HTTP |
| 1 | Resources | Multiple URIs (resources), still one verb |
| 2 | HTTP Verbs | Proper use of methods + status codes (where most “REST” APIs live) |
| 3 | Hypermedia | HATEOAS — links drive state transitions |
Versioning
APIs evolve; versioning protects existing consumers from breaking changes. Common strategies:
- URI path —
/v1/users,/v2/users(most common, most visible). - Header —
Accept: application/vnd.example.v2+json(cleaner URLs, harder to test in a browser). - Query param —
/users?version=2(simple but pollutes the query).
Bump the version only for breaking changes; additive changes (new optional fields, new endpoints) should be backward compatible.
Pagination, filtering, sorting
Never return an unbounded list. Common pagination styles:
- Offset/limit —
GET /users?limit=20&offset=40. Simple, but slow and inconsistent on large, changing datasets. - Cursor/keyset —
GET /users?limit=20&cursor=eyJpZCI6NDJ9. Stable and fast for infinite scroll; the cursor encodes the position.
{
"data": [ { "id": 41 }, { "id": 42 } ],
"pagination": {
"limit": 20,
"nextCursor": "eyJpZCI6NjJ9",
"hasMore": true
}
}
Filtering and sorting via query params: GET /orders?status=paid&sort=-createdAt&minTotal=100.
Idempotency
For non-idempotent operations (like POST payments), let clients send an idempotency key so a retried request does not create a duplicate:
POST /v1/payments
Idempotency-Key: 5f3a9c2e-...
{ "amount": 4998, "currency": "usd" }
The server stores the key with the result; a repeat with the same key returns the original response instead of charging twice. This is how Stripe and others make retries safe.
A full JSON example
GET /v1/products?category=books&limit=2 HTTP/1.1
Accept: application/json
{
"data": [
{
"id": "prd_1",
"name": "Clean Architecture",
"price": { "amount": 3499, "currency": "usd" },
"inStock": true
},
{
"id": "prd_2",
"name": "Designing Data-Intensive Applications",
"price": { "amount": 5499, "currency": "usd" },
"inStock": false
}
],
"pagination": { "limit": 2, "nextCursor": "eyJpZCI6InByZF8yIn0", "hasMore": true }
}
GraphQL
GraphQL is a query language and runtime for APIs, created at Facebook and now under the GraphQL Foundation. Instead of many endpoints, it exposes a single endpoint and a strongly-typed schema; clients ask for exactly the fields they need in one round trip.
Schema
The schema is the contract, written in SDL (Schema Definition Language):
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
}
enum OrderStatus { PENDING PAID CANCELLED }
type Query {
user(id: ID!): User
users(limit: Int = 20): [User!]!
}
type Mutation {
createOrder(userId: ID!, productId: ID!): Order!
}
type Subscription {
orderStatusChanged(orderId: ID!): Order!
}
- Query — read operations.
- Mutation — write operations.
- Subscription — a long-lived stream of updates (usually over WebSocket) for real-time data.
Queries and resolvers
A query asks for a precise shape; the response mirrors it exactly:
query GetUserWithOrders {
user(id: "42") {
name
email
orders {
id
total
status
}
}
}
{
"data": {
"user": {
"name": "Ada",
"email": "ada@example.com",
"orders": [
{ "id": "ord_1", "total": 49.98, "status": "PAID" }
]
}
}
}
Each field is backed by a resolver — a function that returns the data for that field. Resolvers compose: the orders field on User is resolved by a separate function that receives the parent User as its argument.
Over-fetching, under-fetching, and N+1
GraphQL’s main selling point is eliminating over-fetching (getting fields you do not need) and under-fetching (needing several REST calls to assemble one screen). The client specifies the exact tree it wants.
The classic GraphQL pitfall is the N+1 problem: resolving a list of N users and then resolving orders for each one fires 1 query for the users plus N queries for their orders. The standard fix is DataLoader — it batches the N per-item lookups into a single batched query and caches within a request.
// Without batching: 1 + N queries
// With DataLoader: 1 (users) + 1 (batched orders) queries
const orderLoader = new DataLoader(async (userIds) => {
const rows = await db.orders.whereIn('userId', userIds);
return userIds.map((id) => rows.filter((r) => r.userId === id));
});
REST vs GraphQL trade-offs
GraphQL shines for client-driven, aggregation-heavy UIs (mobile apps, dashboards). Costs: HTTP caching is harder (everything is a POST to one endpoint), query complexity must be bounded to prevent abuse, and the server is more complex. REST keeps simple CRUD simple and leverages HTTP caching for free.
gRPC
gRPC is a high-performance RPC framework from Google that uses Protocol Buffers (protobuf) for the schema/serialization and HTTP/2 for transport. You define services and messages in a .proto file, then generate strongly-typed client and server code in many languages.
syntax = "proto3";
package shop.v1;
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc WatchOrders(WatchRequest) returns (stream Order); // server streaming
}
message GetOrderRequest { string id = 1; }
message Order {
string id = 1;
double total = 2;
Status status = 3;
enum Status { PENDING = 0; PAID = 1; CANCELLED = 2; }
}
Key properties:
- Binary protobuf is far more compact and faster to (de)serialize than JSON.
- HTTP/2 brings multiplexing, header compression, and native streaming: unary, server-streaming, client-streaming, and bidirectional-streaming.
- Code generation gives you type-safe stubs on both ends.
When to use gRPC: internal service-to-service communication in a microservices/polyglot backend, low-latency and high-throughput paths, and streaming workloads. When not to: public browser-facing APIs (browsers cannot speak raw gRPC without gRPC-Web and a proxy), or when human-readable payloads and easy curl-ability matter.
SOAP (legacy)
SOAP (Simple Object Access Protocol) is an older, XML-based messaging protocol. Contracts are described in WSDL, messages are XML envelopes, and it brings a heavy set of WS-* standards (WS-Security, WS-ReliableMessaging, etc.). It is transport-agnostic (often HTTP, sometimes message queues) and strongly typed.
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<getOrder xmlns="http://example.com/shop">
<id>ord_789</id>
</getOrder>
</soap:Body>
</soap:Envelope>
You will mostly meet SOAP in enterprise, banking, and legacy government systems. For new work, prefer REST, GraphQL, or gRPC — but knowing SOAP exists (and why its formality was once valuable) is useful.
API documentation and contracts
OpenAPI / Swagger
OpenAPI (formerly Swagger) is the standard, language-agnostic specification for describing REST APIs. A single YAML/JSON document describes paths, methods, parameters, request/response schemas, auth, and examples. From it you can generate interactive docs (Swagger UI), client SDKs, server stubs, and contract tests.
openapi: 3.0.3
info:
title: Shop API
version: 1.0.0
paths:
/v1/orders/{id}:
get:
summary: Get an order by ID
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
'200':
description: The order
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Not found
components:
schemas:
Order:
type: object
properties:
id: { type: string }
total: { type: number, format: float }
status: { type: string, enum: [pending, paid, cancelled] }
required: [id, total, status]
For GraphQL, the schema itself is the contract and enables introspection-driven tooling (GraphiQL, Apollo Studio). For gRPC, the .proto file is the contract. In all three, the machine-readable contract — not prose — is the source of truth.
Cross-cutting concerns
Authentication and authorization
Every non-public API needs to know who is calling (authentication) and what they may do (authorization). Common mechanisms: API keys, OAuth 2.0 / OpenID Connect, and JWT bearer tokens. See Authentication & Authorization and Web Security for depth. Always transmit credentials over TLS and never in the URL.
CORS
CORS (Cross-Origin Resource Sharing) is a browser security mechanism controlling which origins may call your API from JavaScript. The browser sends a preflight OPTIONS request for non-simple requests; your server must respond with the right Access-Control-Allow-* headers.
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
CORS only affects browsers; it is not a substitute for server-side authorization.
Rate limiting and throttling
Protect your API from abuse and overload by capping how many requests a client may make in a window (token-bucket, sliding-window, etc.). Communicate limits with standard headers and return 429 Too Many Requests with Retry-After when exceeded:
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 1000
RateLimit-Remaining: 0
RateLimit-Reset: 42
Retry-After: 42
Error formats (RFC 7807 / RFC 9457)
Return errors in a consistent, machine-readable shape. RFC 7807 “Problem Details for HTTP APIs” (updated by RFC 9457) defines application/problem+json:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://example.com/probs/validation",
"title": "Validation failed",
"status": 422,
"detail": "quantity must be greater than 0",
"instance": "/v1/orders",
"errors": [
{ "field": "quantity", "message": "must be > 0" }
]
}
A stable error contract lets clients program against errors instead of parsing prose.
Content negotiation
Clients declare what they can accept and send; servers respond accordingly. Driven by headers:
Accept: application/json— client wants JSON back.Content-Type: application/json— the request body is JSON.Accept-Language,Accept-Encoding— negotiate language and compression (e.g.,gzip,br).
If the server cannot honor the Accept header it responds 406 Not Acceptable.
Real-time
REST/GraphQL request-response does not fit push-based, real-time needs. For those, reach for WebSockets, SSE, GraphQL subscriptions, or gRPC streaming — see Real-time Communication.
Comparison: REST vs GraphQL vs gRPC vs SOAP
| Dimension | REST | GraphQL | gRPC | SOAP |
|---|---|---|---|---|
| Transport | HTTP/1.1, HTTP/2 | HTTP (single endpoint) | HTTP/2 | HTTP, SMTP, MQ |
| Payload format | JSON (usually), any | JSON | Protobuf (binary) | XML |
| Contract | OpenAPI (optional) | Schema (SDL, required) | .proto (required) | WSDL (required) |
| Coupling | Loose | Medium | Tight (generated stubs) | Tight |
| Streaming | Limited (SSE/chunked) | Subscriptions | First-class (4 modes) | No (natively) |
| Caching | Excellent (HTTP) | Hard (client-side) | Limited | Limited |
| Browser-friendly | Yes | Yes | No (needs gRPC-Web) | Yes (verbose) |
| Best use case | Public/CRUD web APIs | Client-driven, aggregated UIs | Internal microservices, low-latency | Legacy/enterprise, WS-* |
| Tooling | Huge, mature | Strong (Apollo, etc.) | Strong (codegen) | Mature but heavy |
Best Practices
- Design the contract first. Agree on OpenAPI/schema/
.protobefore coding; treat it as the source of truth and review it like code. - Use nouns and proper HTTP semantics. Resource-oriented URLs, correct methods, and precise status codes. No verbs in REST paths.
- Version from day one and only break within a new major version; keep additive changes backward compatible.
- Always paginate collections; prefer cursor pagination for large, changing datasets.
- Standardize errors (RFC 7807/9457) and never leak stack traces or internals to clients.
- Make writes safe to retry with idempotency keys for
POST; usePUT/DELETEsemantics where they fit. - Secure by default: TLS everywhere, authenticate every request, authorize per-resource, validate all input, and set restrictive CORS.
- Rate-limit and throttle with clear headers and
429responses; protect expensive operations. - Bound query cost in GraphQL (depth/complexity limits, persisted queries) and solve N+1 with DataLoader.
- Prefer gRPC for internal service-to-service hot paths; expose REST/GraphQL at the edge for clients.
- Document with examples, generate interactive docs, and keep docs in sync via generation from the contract.
- Be consistent across the whole API surface: naming, casing (pick
snake_caseorcamelCaseand stick to it), date formats (ISO 8601), and envelope shape.
References
- Roy Fielding — Architectural Styles and the Design of Network-based Software Architectures (REST dissertation)
- MDN — HTTP overview and methods
- OpenAPI Specification
- GraphQL — official documentation
- gRPC — official documentation
- Protocol Buffers documentation
- RFC 9457 — Problem Details for HTTP APIs
- Richardson Maturity Model (Martin Fowler)