Chào mừng bạn đến với Bài 4 của khoá học Python FastAPI. Ở ba bài trước, chúng ta đã tập trung vào Python — ngôn ngữ. Ở bài này, chúng ta tạm gác Python sang một bên và nói về một chủ đề mà bất kỳ ai xây dựng web service cũng phải nắm vững: API và HTTP.

Nghe có vẻ cơ bản? Đúng vậy. Nhưng mình đã gặp rất nhiều developer viết FastAPI hàng ngày mà vẫn mơ hồ về: khi nào dùng POST vs PUT, status code 401 khác 403 ra sao, CORS là gì, hay idempotency nghĩa là gì. Những lỗ hổng kiến thức này thường dẫn đến API thiết kế tệ, bug khó debug, và vấn đề bảo mật nghiêm trọng.

Hãy cùng lấp đầy những lỗ hổng đó! 💪

1. API là gì?

API (Application Programming Interface) là một giao diện cho phép hai phần mềm giao tiếp với nhau. Hãy nghĩ về nó như một “thực đơn nhà hàng” — bạn (client) không cần biết đầu bếp nấu thế nào, chỉ cần biết có thể gọi món gì và món đó sẽ đến như thế nào.

Có nhiều loại API khác nhau, nhưng trong khoá học này chúng ta tập trung vào Web API — cụ thể là API giao tiếp qua HTTP:

  • REST API: phong cách phổ biến nhất, dựa trên HTTP và các nguyên tắc REST (chúng ta sẽ nói kỹ ở phần sau).
  • GraphQL: client tự định nghĩa dữ liệu cần lấy, linh hoạt hơn REST trong một số trường hợp.
  • gRPC: hiệu suất cao, dùng Protocol Buffers, thường cho giao tiếp giữa các microservice.
  • WebSocket: giao tiếp hai chiều real-time, khác với mô hình request/response của HTTP thông thường.

FastAPI hỗ trợ tốt cả REST, WebSocket, và có thể tích hợp với GraphQL qua các thư viện như Strawberry.

2. HTTP — Ngôn ngữ chung của web

HTTP (HyperText Transfer Protocol) là giao thức nền tảng của World Wide Web. Mỗi khi bạn mở trình duyệt và truy cập một trang web, gọi một API, hay upload file — bạn đang dùng HTTP (hoặc HTTPS, là HTTP có mã hoá).

HTTP hoạt động theo mô hình request/response:

Client (trình duyệt, mobile app, Postman…) gửi một request đến server. Server xử lý request và trả về một response. Sau đó kết nối có thể đóng lại — HTTP là giao thức stateless, mỗi request độc lập với request khác.

Một HTTP request điển hình trông như thế này:

POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
Accept: application/json

{
  "name": "Joseph",
  "email": "joseph@example.com"
}

Response từ server:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/42
Content-Length: 89

{
  "id": 42,
  "name": "Joseph",
  "email": "joseph@example.com",
  "created_at": "2026-04-18T10:00:00Z"
}

Bốn thành phần quan trọng bạn cần nắm: method (POST), path (/api/users), headers (Content-Type, Authorization…), và body (JSON payload).

3. HTTP Methods

HTTP methods (hay còn gọi là verbs) cho server biết client muốn làm gì. Có 9 methods tiêu chuẩn, nhưng trong thực tế REST API bạn sẽ dùng chủ yếu 5 cái:

from fastapi import FastAPI

app = FastAPI()

# GET - Lấy dữ liệu, KHÔNG thay đổi gì ở server
@app.get("/users")
def list_users():
    return [{"id": 1, "name": "Alice"}]

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": "Alice"}

# POST - Tạo mới resource
@app.post("/users")
def create_user(user: dict):
    return {"id": 42, **user}

# PUT - Thay thế toàn bộ resource
@app.put("/users/{user_id}")
def replace_user(user_id: int, user: dict):
    return {"id": user_id, **user}

# PATCH - Cập nhật một phần resource
@app.patch("/users/{user_id}")
def update_user(user_id: int, updates: dict):
    return {"id": user_id, **updates}

# DELETE - Xoá resource
@app.delete("/users/{user_id}")
def delete_user(user_id: int):
    return {"deleted": user_id}

PUT vs PATCH — khác biệt thường bị nhầm

Đây là lỗi cực kỳ phổ biến.

  • PUT nghĩa là “thay thế toàn bộ” — bạn phải gửi toàn bộ resource.
  • PATCH nghĩa là “cập nhật một phần” — chỉ gửi field cần thay đổi.
# User hiện tại: {id: 1, name: "Alice", email: "alice@x.com", age: 30}

# PUT - phải gửi đầy đủ, nếu thiếu age thì age sẽ bị xoá/reset
PUT /users/1
{"name": "Alice Smith", "email": "alice@x.com", "age": 30}

# PATCH - chỉ gửi field muốn đổi
PATCH /users/1
{"name": "Alice Smith"}

Idempotency — tính chất cực kỳ quan trọng

Một method được gọi là idempotent nếu gọi nó nhiều lần có kết quả giống như gọi một lần. Đây là tính chất quan trọng để xử lý retry khi mạng không ổn định.

GET, PUT, DELETE là idempotent.

POST và PATCH không idempotent.

Ví dụ: gọi POST /users 3 lần sẽ tạo ra 3 user; gọi PUT /users/1 3 lần chỉ có 1 user với dữ liệu cuối cùng.

4. HTTP Status Codes

Status code là con số 3 chữ số mà server trả về để báo kết quả xử lý request. Chúng được chia thành 5 nhóm:

  • 1xx – Informational (hiếm khi gặp).
  • 2xx – Success.
  • 3xx – Redirect.
  • 4xx – Client error (lỗi do client).
  • 5xx – Server error (lỗi do server).

Các status code quan trọng nhất

from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse

app = FastAPI()

# 200 OK - Mặc định cho mọi response thành công
@app.get("/users/{id}")
def get_user(id: int):
    return {"id": id}

# 201 Created - Tạo mới thành công
@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: dict):
    return {"id": 42, **user}

# 204 No Content - Xoá thành công, không trả về body
@app.delete("/users/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(id: int):
    return None

# 400 Bad Request - Request không hợp lệ
# 401 Unauthorized - Chưa xác thực (chưa đăng nhập)
# 403 Forbidden - Đã xác thực nhưng KHÔNG có quyền
# 404 Not Found - Không tìm thấy resource
# 409 Conflict - Xung đột (vd: email đã tồn tại)
# 422 Unprocessable Entity - Dữ liệu không qua được validation
# 429 Too Many Requests - Rate limit

@app.get("/users/{id}")
def get_user(id: int):
    user = db.get(id)
    if not user:
        raise HTTPException(status_code=404, detail="User không tồn tại")
    return user

# 500 Internal Server Error - Lỗi phía server
# 502 Bad Gateway - Server upstream lỗi
# 503 Service Unavailable - Server quá tải hoặc đang bảo trì

401 vs 403 — phân biệt rõ ràng

Đây cũng là một nhầm lẫn phổ biến:

  • 401 Unauthorized = “Tôi không biết bạn là ai” — chưa có token, token sai, hoặc hết hạn. Client cần đăng nhập lại.
  • 403 Forbidden = “Tôi biết bạn là ai, nhưng bạn không có quyền làm việc này” — đã xác thực thành công nhưng không đủ permission. Ví dụ: user thường cố truy cập endpoint admin.

5. Headers — Metadata của request/response

Headers chứa thông tin về request/response. Một số header quan trọng:

from fastapi import FastAPI, Header, Response
from typing import Optional

app = FastAPI()

@app.get("/items")
def read_items(
    # Header thường dùng cho authentication
    authorization: Optional[str] = Header(None),
    # Header cho phân trang
    x_page: int = Header(1),
    # Accept-Language cho i18n
    accept_language: Optional[str] = Header(None),
):
    return {
        "auth": authorization,
        "page": x_page,
        "lang": accept_language,
    }

# Set header trong response
@app.get("/download")
def download(response: Response):
    response.headers["X-Rate-Limit"] = "100"
    response.headers["Cache-Control"] = "max-age=3600"
    return {"data": "..."}

Các header phổ biến bạn sẽ gặp nhiều:

  • Content-Type: báo định dạng body (application/json, multipart/form-data…).
  • Authorization: chứa token xác thực (Bearer token, Basic auth…).
  • Accept: client muốn nhận định dạng gì.
  • Cache-Control: điều khiển caching.
  • X-Request-ID: dùng cho tracing, debug.

Header bắt đầu bằng X- là convention cũ cho custom header (hiện không còn bắt buộc nhưng vẫn phổ biến).

6. Query Parameters, Path Parameters, và Body

FastAPI phân biệt rõ ba cách truyền dữ liệu từ client:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
    name: str
    email: str

# Path parameter: /users/42
@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id}

# Query parameter: /users?page=2&limit=10
@app.get("/users")
def list_users(page: int = 1, limit: int = 10, search: str = ""):
    return {"page": page, "limit": limit, "search": search}

# Request body: dữ liệu trong body
@app.post("/users")
def create_user(user: UserCreate):
    return user

# Kết hợp cả ba
@app.put("/users/{user_id}")
def update_user(
    user_id: int,               # path
    notify: bool = False,       # query
    user: UserCreate = ...,     # body
):
    return {"id": user_id, "notify": notify, **user.dict()}

Quy tắc sử dụng: dùng path để xác định resource cụ thể. Dùng query cho filter, sort, pagination, options. Dùng body cho dữ liệu tạo/cập nhật resource.

7. Content Types

Header Content-Type cho server biết body đang ở định dạng gì. Ba loại phổ biến nhất:

from fastapi import FastAPI, File, Form, UploadFile
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    email: str

# application/json - phổ biến nhất cho REST API
@app.post("/users")
def create_user(user: User):
    return user

# application/x-www-form-urlencoded - form HTML truyền thống
@app.post("/login")
def login(username: str = Form(...), password: str = Form(...)):
    return {"username": username}

# multipart/form-data - upload file
@app.post("/upload")
def upload(file: UploadFile = File(...), description: str = Form("")):
    return {
        "filename": file.filename,
        "size": file.size,
        "description": description,
    }

8. REST — Kiến trúc, không phải giao thức

REST (Representational State Transfer) là một bộ nguyên tắc thiết kế API do Roy Fielding đề xuất năm 2000. REST KHÔNG phải là protocol hay standard — nó là một phong cách kiến trúc.

Sáu nguyên tắc của REST:

  • 1. Client-Server: tách biệt rõ client và server.
  • 2. Stateless: mỗi request chứa đầy đủ thông tin cần thiết, server không lưu state của client.
  • 3. Cacheable: response có thể được cache.
  • 4. Uniform Interface: dùng URL, HTTP methods, status codes một cách nhất quán.
  • 5. Layered System: client không biết đang nói chuyện với server gốc hay proxy/cache.
  • 6. Code on Demand (tuỳ chọn): server có thể gửi code cho client thực thi.

Resource-oriented design

REST API xoay quanh khái niệm resource (tài nguyên). Mỗi resource có một URI, và bạn dùng HTTP methods để thao tác với chúng:

# TỐT - resource-oriented, dùng danh từ (số nhiều)
GET    /users              # Lấy danh sách
GET    /users/42           # Lấy user có id=42
POST   /users              # Tạo user mới
PUT    /users/42           # Cập nhật toàn bộ user 42
PATCH  /users/42           # Cập nhật một phần user 42
DELETE /users/42           # Xoá user 42

# Nested resource - resource thuộc resource khác
GET    /users/42/posts            # Posts của user 42
POST   /users/42/posts            # Tạo post cho user 42
GET    /users/42/posts/7          # Post cụ thể

# KHÔNG TỐT - dùng động từ, không nhất quán
GET    /getUsers
POST   /createUser
GET    /user/delete/42
POST   /user_update?id=42

Thiết kế URL tốt

Một vài quy tắc tốt:

  • Dùng danh từ số nhiều cho collection: /users, không phải /user.
  • Dùng kebab-case cho multi-word: /order-items, không phải /orderItems hay /order_items.
  • Không dùng động từ trong URL — HTTP method đã là động từ rồi.
  • Giữ URL ngắn gọn và có thể đoán được.

Khi không map được vào resource chuẩn, đôi khi bạn cần các action URL (không phải REST thuần nhưng thực dụng):

# Action không phải CRUD
POST /users/42/deactivate
POST /orders/7/cancel
POST /auth/login
POST /auth/logout

9. API Versioning

API một khi đã public, bạn không thể thay đổi tuỳ tiện — client đang dùng sẽ bị vỡ. Đó là lý do cần versioning. Có ba cách phổ biến:

from fastapi import FastAPI, Header, APIRouter

app = FastAPI()

# Cách 1: URL path (phổ biến nhất, dễ hiểu)
v1 = APIRouter(prefix="/api/v1")
v2 = APIRouter(prefix="/api/v2")

@v1.get("/users")
def list_users_v1():
    return [{"id": 1, "name": "Alice"}]

@v2.get("/users")
def list_users_v2():
    # v2 thêm field email
    return [{"id": 1, "name": "Alice", "email": "alice@x.com"}]

app.include_router(v1)
app.include_router(v2)

# Cách 2: Header
@app.get("/users")
def list_users(api_version: str = Header("1")):
    if api_version == "2":
        return [{"id": 1, "name": "Alice", "email": "alice@x.com"}]
    return [{"id": 1, "name": "Alice"}]

# Cách 3: Query parameter (ít dùng)
# GET /users?version=2

Mình khuyên dùng cách 1 (URL path) vì nó rõ ràng, dễ cache, dễ test bằng trình duyệt hay curl.

10. CORS — Cross-Origin Resource Sharing

Đây là thứ mà 99% developer sẽ gặp khi frontend và backend chạy trên domain khác nhau. Trình duyệt có chính sách same-origin — theo mặc định, JavaScript từ app.example.com không được phép gọi API ở api.example.com.

CORS là cơ chế cho phép server “tuyên bố” rằng mình chấp nhận request từ origin khác. FastAPI có middleware sẵn để xử lý:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://app.example.com",
        "http://localhost:3000",  # cho development
    ],
    allow_credentials=True,
    allow_methods=["*"],       # hoặc ["GET", "POST", "PUT", "DELETE"]
    allow_headers=["*"],
)

Một sai lầm bảo mật phổ biến: đặt allow_origins=["*"] trên production. Điều này cho phép MỌI website gọi API của bạn — cực kỳ nguy hiểm. Chỉ dùng * cho API hoàn toàn public không có authentication.

11. Authentication vs Authorization

Hai khái niệm này rất dễ nhầm:

  • Authentication (xác thực) = “Bạn là ai?” — verify danh tính. Thường qua username/password, token, OAuth…
  • Authorization (uỷ quyền) = “Bạn được phép làm gì?” — kiểm tra quyền sau khi đã xác thực.

Các phương pháp authentication phổ biến trong REST API:

# Basic Auth - username:password encode base64 (chỉ dùng với HTTPS)
Authorization: Basic am9zZXBoOnNlY3JldA==

# Bearer Token - JWT hoặc opaque token, phổ biến nhất hiện nay
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# API Key - thường cho machine-to-machine
X-API-Key: sk_live_abcd1234...

# OAuth 2.0 - dùng khi cần user đăng nhập bằng Google/Facebook/...

Chúng ta sẽ đi sâu vào authentication/authorization ở Bài 9. Giờ chỉ cần nhớ: chúng là hai việc KHÁC NHAU, thường làm tuần tự (auth trước, authz sau).

12. HTTPS và bảo mật cơ bản

Mọi API public đều PHẢI dùng HTTPS, không có ngoại lệ. Lý do:

HTTP truyền plain text — ai bắt được packet trên mạng đều đọc được password, token. HTTPS mã hoá toàn bộ request/response. HTTPS cũng verify server không bị giả mạo (MITM attack).

Ngoài HTTPS, một vài nguyên tắc bảo mật cơ bản:

  • Không bao giờ để password, API key, token trong URL (chúng sẽ bị log vào server log, browser history, proxy…).
  • Luôn validate input ở server — không tin tưởng bất cứ gì từ client.
  • Rate limiting để chống brute force và DoS.
  • CSRF protection cho endpoint thay đổi state (nếu dùng cookie-based auth).

13. Công cụ thực hành

Khi xây dựng và debug API, bạn cần các công cụ để gửi HTTP request thủ công:

curl: command line, có sẵn trên mọi hệ điều hành Unix-like. Đơn giản, script-friendly.

# GET
curl http://localhost:8000/users

# POST với JSON
curl -X POST http://localhost:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Joseph", "email": "joseph@example.com"}'

# Thêm header authentication
curl http://localhost:8000/users \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

# Verbose để xem headers và status code
curl -v http://localhost:8000/users

HTTPie: giống curl nhưng cú pháp thân thiện hơn.

http POST localhost:8000/users name=Joseph email=joseph@example.com
http GET localhost:8000/users Authorization:"Bearer TOKEN"
  • Postman / Insomnia / Bruno: GUI clients, lưu được collection, environment, có thể share với team. Rất tiện khi phát triển API phức tạp.
  • Swagger UI / ReDoc: FastAPI tự động tạo cả hai. Vào http://localhost:8000/docs là có giao diện tương tác ngay. Đây là một trong những tính năng “bán hàng” nhất của FastAPI.

14. Thiết kế API tốt — checklist

Trước khi kết thúc, đây là checklist mình dùng khi review hoặc thiết kế REST API:

✅ URL dùng danh từ số nhiều, không dùng động từ.

✅ Dùng đúng HTTP method cho đúng mục đích (GET không được thay đổi state!).

✅ Trả về status code phù hợp (không phải cái gì cũng trả 200).

✅ Response JSON có cấu trúc nhất quán giữa các endpoint.

✅ Lỗi trả về format chuẩn (có message, có error code).

✅ Có versioning từ ngày đầu (/api/v1/...).

✅ Có pagination cho endpoint trả về list.

✅ Validation đầu vào chặt chẽ.

✅ HTTPS, authentication, rate limiting sẵn sàng.

✅ Documentation tự động (Swagger/ReDoc).

May mắn là FastAPI đã lo sẵn nhiều mục trong checklist này cho bạn — validation, documentation, status codes. Việc của bạn là dùng chúng đúng cách.

Tổng kết

Trong bài này, chúng ta đã đi qua nền tảng API và HTTP — kiến thức cốt lõi mà mọi backend developer cần nắm:

HTTP là giao thức request/response stateless, nền tảng của web. Các HTTP methods (GET, POST, PUT, PATCH, DELETE) mỗi cái có ngữ nghĩa riêng — idempotency là tính chất quan trọng. Status codes chia thành 5 nhóm — nắm rõ 401 vs 403, 200 vs 201 vs 204. Headers mang metadata, body mang dữ liệu. REST là phong cách kiến trúc xoay quanh resource. CORS, HTTPS, authentication là những chủ đề bảo mật không thể bỏ qua.

Bài 5 tiếp theo, chúng ta sẽ quay lại FastAPI và đi sâu vào các tính năng cơ bản của nó — cài đặt môi trường chuẩn, cấu trúc project, cách định nghĩa endpoint, request validation với Pydantic, response model, dependency injection đơn giản, và nhiều tính năng khác. Từ bài đó trở đi, chúng ta sẽ bắt đầu xây dựng một ứng dụng thực tế xuyên suốt các bài còn lại.

Leave a Reply

Discover more from Bệ Phóng Việt

Subscribe now to keep reading and get access to the full archive.

Continue reading