Back to blog

Dependency Injection trong FastAPI: Hướng Dẫn Toàn Diện

pythonfastapidependency-injectiontestingarchitecture
Dependency Injection trong FastAPI: Hướng Dẫn Toàn Diện

Dependency Injection (DI) là một trong những tính năng mạnh mẽ nhất của FastAPI, nhưng thường bị hiểu sai. Hệ thống DI của FastAPI cho phép bạn viết code sạch, modular và dễ test một cách dễ dàng. Hãy cùng làm chủ nó từ cơ bản đến nâng cao.

Dependency Injection là gì?

Dependency Injection là một design pattern trong đó các component nhận dependencies từ nguồn bên ngoài thay vì tự tạo chúng bên trong.

# ❌ Without Dependency Injection
def get_user(user_id: int):
    db = Database()  # Creating dependency internally
    return db.query(User).filter(User.id == user_id).first()
 
# ✅ With Dependency Injection
def get_user(user_id: int, db: Database = Depends(get_db)):
    return db.query(User).filter(User.id == user_id).first()

Lợi ích

  1. Dễ test - Dễ dàng mock dependencies
  2. Tái sử dụng - Chia sẻ code giữa các endpoint
  3. Tách biệt trách nhiệm - Ranh giới component rõ ràng
  4. Dễ bảo trì - Thay đổi ở một nơi
  5. Type Safety - Hỗ trợ đầy đủ từ editor

Dependencies Cơ Bản

Simple Function Dependency

from fastapi import Depends, FastAPI
 
app = FastAPI()
 
def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
    """Common query parameters."""
    return {"q": q, "skip": skip, "limit": limit}
 
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    """Use common parameters dependency."""
    return commons
 
@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    """Reuse the same dependency."""
    return commons

Khi bạn gọi /items/?q=test&skip=10&limit=50, FastAPI sẽ:

  1. Gọi common_parameters(q="test", skip=10, limit=50)
  2. Trả kết quả về commons
  3. Function của bạn nhận data đã được xử lý

Database Session Dependency

from sqlalchemy.orm import Session
from database import SessionLocal
 
def get_db():
    """Database session dependency."""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
 
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: Session = Depends(get_db)):
    """Get user with automatic DB session management."""
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

Điểm quan trọng:

  • yield biến nó thành generator (context manager)
  • Code sau yield chạy sau khi response được trả về
  • Hoàn hảo cho cleanup (đóng connection, file, v.v.)

Class-Based Dependencies

from typing import Optional
 
class CommonQueryParams:
    """Reusable query parameters as a class."""
    
    def __init__(
        self,
        q: Optional[str] = None,
        skip: int = 0,
        limit: int = 100
    ):
        self.q = q
        self.skip = skip
        self.limit = limit
 
@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    """Use class-based dependency."""
    response = {"skip": commons.skip, "limit": commons.limit}
    if commons.q:
        response["q"] = commons.q
    return response
 
# Shorthand syntax (same result)
@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):
    """FastAPI infers the dependency from the type hint."""
    return commons

Sub-Dependencies (Dependencies của Dependencies)

from fastapi import Header, HTTPException
 
def verify_token(x_token: str = Header(...)):
    """Verify authentication token."""
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="Invalid X-Token header")
    return x_token
 
def verify_key(x_key: str = Header(...)):
    """Verify API key."""
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="Invalid X-Key header")
    return x_key
 
def get_current_user(
    token: str = Depends(verify_token),
    key: str = Depends(verify_key),
    db: Session = Depends(get_db)
):
    """
    Get current user - depends on verify_token, verify_key, and get_db.
    FastAPI resolves all sub-dependencies automatically.
    """
    # Both token and key are verified before this runs
    user = db.query(User).filter(User.token == token).first()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user
 
@app.get("/users/me")
async def read_user_me(current_user: User = Depends(get_current_user)):
    """
    This endpoint requires:
    1. Valid X-Token header (via verify_token)
    2. Valid X-Key header (via verify_key)
    3. Database session (via get_db)
    4. User exists in database (via get_current_user)
    
    All checked automatically by FastAPI!
    """
    return current_user

Cây phụ thuộc (Dependency Tree):

read_user_me
    └── get_current_user
            ├── verify_token
            ├── verify_key
            └── get_db

Dependencies trong Path Operation Decorators

from fastapi import Depends
 
# Apply dependency to all routes in the decorator
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    """This route requires token and key, but doesn't need their return values."""
    return [{"item_id": "Foo"}]
 
# Multiple dependencies
@app.get(
    "/users/",
    dependencies=[
        Depends(verify_token),
        Depends(verify_key),
        Depends(check_subscription)
    ]
)
async def read_users():
    """Multiple dependencies for access control."""
    return [{"username": "Rick"}, {"username": "Morty"}]

Sử dụng khi:

  • Bạn cần dependency chạy (ví dụ: validation)
  • Bạn không cần giá trị trả về
  • Bạn muốn function signature gọn hơn

Global Dependencies

from fastapi import FastAPI, Depends
 
app = FastAPI(
    dependencies=[Depends(verify_token)]
)
 
# Now ALL routes require the token
@app.get("/items/")
async def read_items():
    return [{"item_id": "Foo"}]
 
@app.get("/users/")
async def read_users():
    return [{"username": "Rick"}]

Dependencies ở cấp Router:

from fastapi import APIRouter
 
router = APIRouter(
    prefix="/admin",
    dependencies=[Depends(verify_admin)]
)
 
@router.get("/users")
async def admin_get_users():
    """Only admins can access this."""
    return users
 
@router.delete("/users/{user_id}")
async def admin_delete_user(user_id: int):
    """Only admins can access this."""
    return {"deleted": user_id}

Yield Dependencies (Context Managers)

Database với Transactions

from sqlalchemy.orm import Session
 
def get_db_with_transaction():
    """Database with automatic transaction management."""
    db = SessionLocal()
    try:
        yield db
        db.commit()  # Commit on success
    except Exception:
        db.rollback()  # Rollback on error
        raise
    finally:
        db.close()  # Always close
 
@app.post("/users/")
async def create_user(
    user: UserCreate,
    db: Session = Depends(get_db_with_transaction)
):
    """User is created in a transaction."""
    new_user = User(**user.dict())
    db.add(new_user)
    # Transaction commits automatically if no exception
    return new_user

Dọn dẹp Resource

import time
from contextlib import contextmanager
 
class Timer:
    """Timer context manager for performance monitoring."""
    
    def __enter__(self):
        self.start = time.time()
        return self
    
    def __exit__(self, *args):
        self.end = time.time()
        self.elapsed = self.end - self.start
 
def get_timer():
    """Timer dependency with automatic cleanup."""
    timer = Timer()
    try:
        with timer:
            yield timer
    finally:
        print(f"Request took {timer.elapsed:.2f} seconds")
 
@app.get("/slow-operation")
async def slow_operation(timer: Timer = Depends(get_timer)):
    """Automatically timed."""
    # Simulate slow operation
    time.sleep(2)
    return {"message": "Done"}

Xử lý File

from pathlib import Path
 
def get_temp_file():
    """Temporary file with automatic cleanup."""
    temp_file = Path("/tmp/temp_file.txt")
    temp_file.touch()
    try:
        yield temp_file
    finally:
        temp_file.unlink(missing_ok=True)
 
@app.post("/process-file/")
async def process_file(
    content: str,
    temp_file: Path = Depends(get_temp_file)
):
    """Process content with temporary file."""
    temp_file.write_text(content)
    # Process file...
    result = temp_file.read_text()
    # File automatically deleted after response
    return {"processed": result}

Dependencies với Parameters

from typing import Optional
 
def pagination(page: int = 1, size: int = 50):
    """Pagination parameters with validation."""
    if page < 1:
        raise HTTPException(status_code=400, detail="Page must be >= 1")
    if size < 1 or size > 100:
        raise HTTPException(status_code=400, detail="Size must be 1-100")
    
    skip = (page - 1) * size
    return {"skip": skip, "limit": size}
 
@app.get("/items/")
async def list_items(
    paging: dict = Depends(pagination),
    db: Session = Depends(get_db)
):
    """List items with pagination."""
    items = db.query(Item).offset(paging["skip"]).limit(paging["limit"]).all()
    return items

Dependencies có tham số (Parameterized Dependencies)

from typing import Optional
 
class RateLimiter:
    """Rate limiting dependency."""
    
    def __init__(self, max_calls: int = 10, period: int = 60):
        self.max_calls = max_calls
        self.period = period
        self.calls = {}
    
    def __call__(self, request: Request):
        """Check rate limit for IP."""
        client_ip = request.client.host
        now = time.time()
        
        # Clean old entries
        self.calls = {
            ip: times 
            for ip, times in self.calls.items() 
            if times[-1] > now - self.period
        }
        
        # Check limit
        if client_ip in self.calls:
            if len(self.calls[client_ip]) >= self.max_calls:
                raise HTTPException(
                    status_code=429,
                    detail="Rate limit exceeded"
                )
            self.calls[client_ip].append(now)
        else:
            self.calls[client_ip] = [now]
        
        return True
 
# Create rate limiter instances
rate_limit_strict = RateLimiter(max_calls=5, period=60)
rate_limit_relaxed = RateLimiter(max_calls=100, period=60)
 
@app.get("/api/strict", dependencies=[Depends(rate_limit_strict)])
async def strict_endpoint():
    """Maximum 5 calls per minute."""
    return {"message": "Success"}
 
@app.get("/api/relaxed", dependencies=[Depends(rate_limit_relaxed)])
async def relaxed_endpoint():
    """Maximum 100 calls per minute."""
    return {"message": "Success"}

Caching Dependencies

from functools import lru_cache
from pydantic_settings import BaseSettings
 
class Settings(BaseSettings):
    """Application settings."""
    app_name: str = "FastAPI App"
    database_url: str
    secret_key: str
    
    class Config:
        env_file = ".env"
 
@lru_cache()
def get_settings():
    """
    Cached settings - loaded only once.
    Perfect for configuration that doesn't change during runtime.
    """
    return Settings()
 
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    """Get app info - settings loaded from cache."""
    return {
        "app_name": settings.app_name,
        "database": settings.database_url
    }

Lưu ý quan trọng:

  • Dùng @lru_cache() cho các operation tốn kém
  • Chỉ cache dependencies không thay đổi
  • Không cache database sessions hoặc data đặc thủ request

Dependency Overrides (Testing)

# main.py
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
 
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: Session = Depends(get_db)):
    return db.query(User).filter(User.id == user_id).first()
# test_main.py
from fastapi.testclient import TestClient
from main import app, get_db
 
# Mock database
def override_get_db():
    """Override database with test database."""
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()
 
# Override the dependency
app.dependency_overrides[get_db] = override_get_db
 
client = TestClient(app)
 
def test_get_user():
    response = client.get("/users/1")
    assert response.status_code == 200
    # Uses test database instead of production database!

Lợi ích:

  • Test với mock data
  • Sử dụng test database
  • Override authentication
  • Tăng tốc độ test

Ví dụ Thực Tế

1. Current User Dependency

from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
 
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db)
) -> User:
    """Extract and validate user from JWT token."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = db.query(User).filter(User.username == username).first()
    if user is None:
        raise credentials_exception
    
    return user
 
async def get_current_active_user(
    current_user: User = Depends(get_current_user)
) -> User:
    """Ensure user is active."""
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user
 
async def get_current_admin_user(
    current_user: User = Depends(get_current_active_user)
) -> User:
    """Ensure user is admin."""
    if not current_user.is_admin:
        raise HTTPException(status_code=403, detail="Not enough permissions")
    return current_user
 
# Use in routes
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user
 
@app.get("/admin/users")
async def read_all_users(
    admin_user: User = Depends(get_current_admin_user),
    db: Session = Depends(get_db)
):
    return db.query(User).all()

2. Pagination Dependency

from typing import Optional
from pydantic import BaseModel
 
class PaginationParams(BaseModel):
    """Pagination parameters with validation."""
    skip: int = 0
    limit: int = 100
    
    def __init__(self, page: int = 1, page_size: int = 50):
        if page < 1:
            raise HTTPException(status_code=400, detail="Page must be >= 1")
        if page_size < 1 or page_size > 100:
            raise HTTPException(status_code=400, detail="Page size must be 1-100")
        
        skip = (page - 1) * page_size
        super().__init__(skip=skip, limit=page_size)
 
@app.get("/items/")
async def list_items(
    pagination: PaginationParams = Depends(),
    db: Session = Depends(get_db)
):
    items = (
        db.query(Item)
        .offset(pagination.skip)
        .limit(pagination.limit)
        .all()
    )
    total = db.query(Item).count()
    
    return {
        "items": items,
        "total": total,
        "page": (pagination.skip // pagination.limit) + 1,
        "page_size": pagination.limit
    }

3. Request Validation

from fastapi import Request
 
async def validate_content_type(request: Request):
    """Ensure request has correct content type."""
    content_type = request.headers.get("content-type")
    if content_type != "application/json":
        raise HTTPException(
            status_code=400,
            detail="Content-Type must be application/json"
        )
    return content_type
 
async def validate_request_size(request: Request):
    """Ensure request body is not too large."""
    content_length = request.headers.get("content-length")
    if content_length and int(content_length) > 1_000_000:  # 1MB
        raise HTTPException(
            status_code=413,
            detail="Request body too large"
        )
    return True
 
@app.post("/upload/", dependencies=[
    Depends(validate_content_type),
    Depends(validate_request_size)
])
async def upload_data(data: dict):
    """Upload with validation."""
    return {"received": data}

4. Service Layer Pattern

class UserService:
    """User business logic."""
    
    def __init__(self, db: Session = Depends(get_db)):
        self.db = db
    
    def get_user(self, user_id: int) -> User:
        user = self.db.query(User).filter(User.id == user_id).first()
        if not user:
            raise HTTPException(status_code=404, detail="User not found")
        return user
    
    def create_user(self, user_data: UserCreate) -> User:
        # Check if user exists
        existing = self.db.query(User).filter(
            User.email == user_data.email
        ).first()
        if existing:
            raise HTTPException(status_code=400, detail="Email already exists")
        
        # Create user
        user = User(**user_data.dict())
        self.db.add(user)
        self.db.commit()
        self.db.refresh(user)
        return user
    
    def update_user(self, user_id: int, user_data: UserUpdate) -> User:
        user = self.get_user(user_id)
        for field, value in user_data.dict(exclude_unset=True).items():
            setattr(user, field, value)
        self.db.commit()
        self.db.refresh(user)
        return user
 
# Use in routes
@app.get("/users/{user_id}")
async def get_user(
    user_id: int,
    user_service: UserService = Depends()
):
    return user_service.get_user(user_id)
 
@app.post("/users/")
async def create_user(
    user_data: UserCreate,
    user_service: UserService = Depends()
):
    return user_service.create_user(user_data)

Patterns Nâng Cao

1. Dependency với State

class ConnectionPool:
    """Connection pool as a dependency."""
    
    def __init__(self):
        self.connections = []
        self.max_size = 10
    
    async def get_connection(self):
        """Get connection from pool."""
        if not self.connections:
            # Create new connection
            return await create_connection()
        return self.connections.pop()
    
    async def return_connection(self, conn):
        """Return connection to pool."""
        if len(self.connections) < self.max_size:
            self.connections.append(conn)
        else:
            await conn.close()
 
# Single instance for the app
pool = ConnectionPool()
 
async def get_connection():
    """Get connection from pool with automatic return."""
    conn = await pool.get_connection()
    try:
        yield conn
    finally:
        await pool.return_connection(conn)
 
@app.get("/data")
async def get_data(conn = Depends(get_connection)):
    return await conn.fetch("SELECT * FROM data")

2. Dependencies có điều kiện (Conditional Dependencies)

from typing import Optional
 
def get_optional_user(
    token: Optional[str] = Header(None),
    db: Session = Depends(get_db)
) -> Optional[User]:
    """Get user if token provided, None otherwise."""
    if not token:
        return None
    
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        return db.query(User).filter(User.username == username).first()
    except JWTError:
        return None
 
@app.get("/items/")
async def list_items(
    user: Optional[User] = Depends(get_optional_user),
    db: Session = Depends(get_db)
):
    """List items - authenticated users see private items."""
    query = db.query(Item)
    
    if user:
        # Show all items for authenticated users
        query = query.filter(
            (Item.is_public == True) | (Item.owner_id == user.id)
        )
    else:
        # Show only public items
        query = query.filter(Item.is_public == True)
    
    return query.all()

3. Kết hợp Dependencies (Dependency Composition)

from dataclasses import dataclass
 
@dataclass
class RequestContext:
    """Aggregated request context."""
    user: User
    db: Session
    settings: Settings
    
async def get_request_context(
    user: User = Depends(get_current_active_user),
    db: Session = Depends(get_db),
    settings: Settings = Depends(get_settings)
) -> RequestContext:
    """Combine multiple dependencies into one context."""
    return RequestContext(user=user, db=db, settings=settings)
 
@app.get("/profile")
async def get_profile(ctx: RequestContext = Depends(get_request_context)):
    """Use aggregated context."""
    # Access all dependencies through ctx
    user_data = ctx.db.query(UserProfile).filter(
        UserProfile.user_id == ctx.user.id
    ).first()
    
    return {
        "user": ctx.user,
        "profile": user_data,
        "app_name": ctx.settings.app_name
    }

Best Practices

1. Giữ Dependencies thuần khiết (Pure)

# ✅ Good: Pure dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
 
# ❌ Bad: Dependency with side effects
def get_db():
    db = SessionLocal()
    log_access()  # Side effect!
    yield db
    db.close()

2. Sử dụng Type Hints

# ✅ Good: Clear types
def get_user(user_id: int, db: Session = Depends(get_db)) -> User:
    return db.query(User).filter(User.id == user_id).first()
 
# ❌ Bad: No type hints
def get_user(user_id, db = Depends(get_db)):
    return db.query(User).filter(User.id == user_id).first()

3. Đặt tên Dependencies rõ ràng

# ✅ Good: Clear names
get_db
get_current_user
verify_token
require_admin
 
# ❌ Bad: Vague names
dep1
check
validate
helper

4. Đừng lạm dụng Dependencies

# ❌ Bad: Dependency for simple parameter
def get_user_id(user_id: int = Path(...)):
    return user_id
 
@app.get("/users/{user_id}")
async def get_user(uid: int = Depends(get_user_id)):
    # Just use user_id directly!
    pass
 
# ✅ Good: Use parameter directly
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # Simple and clear
    pass

5. Sử dụng Sub-Dependencies để kết hợp

# ✅ Good: Composed dependencies
def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db)
) -> User:
    # Uses sub-dependencies
    pass
 
# ❌ Bad: Everything in one dependency
def get_current_user(
    authorization: str = Header(...),
) -> User:
    # Parsing token, connecting to DB, querying - all in one function
    # Hard to test and reuse
    pass

Testing với Dependencies

# conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
 
from main import app, get_db
from database import Base
 
# Test database
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
 
def override_get_db():
    """Test database dependency."""
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()
 
@pytest.fixture
def client():
    """Test client with overridden dependencies."""
    Base.metadata.create_all(bind=engine)
    app.dependency_overrides[get_db] = override_get_db
    
    with TestClient(app) as c:
        yield c
    
    Base.metadata.drop_all(bind=engine)
    app.dependency_overrides.clear()
 
# test_users.py
def test_create_user(client):
    response = client.post("/users/", json={
        "username": "test",
        "email": "[email protected]"
    })
    assert response.status_code == 201
    # Uses test database!

Kết Luận

Dependency Injection trong FastAPI mạnh mẽ mà vẫn trực quan:

✅ Code sạch - Không có global state hay tight coupling
✅ Dễ test - Dễ dàng override cho testing
✅ Tái sử dụng - Chia sẻ logic giữa các endpoint
✅ Type safety - Hỗ trợ đầy đủ từ editor
✅ Tự động thực thi - FastAPI lo phần còn lại
✅ Sub-dependencies - Kết hợp logic phức tạp
✅ Context managers - Tự động cleanup với yield

Điểm chính cần nhớ

  1. Dùng Depends() để inject dependencies
  2. Yield cho cleanup - Hoàn hảo cho resources như DB sessions
  3. Sub-dependencies kết hợp tốt với nhau
  4. Classes hoạt động tuyệt vời như dependencies
  5. Override trong tests để mocking
  6. Cache các operations tốn kém với @lru_cache()
  7. Giữ dependencies thuần khiết - Tránh side effects
  8. Type hints giúp ích cho mọi người

Bước tiếp theo

  • Implement service layer pattern với DI
  • Tạo custom dependency decorators
  • Xây dựng middleware sử dụng dependencies
  • Tối ưu hóa với caching strategies
  • Thêm monitoring với dependency wrappers
  • Tạo dependency validation layers

Làm chủ Dependency Injection và code FastAPI của bạn sẽ sạch hơn, dễ test hơn, và dễ bảo trì hơn! 🚀

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.