Dependency Injection
What problem does DI solve?
Section titled “What problem does DI solve?”In every API you have shared concerns: getting a DB session, reading the current user from a token, or validating common query parameters. In Express you solve this with middleware. In NestJS you use its IoC container with @Injectable() providers. FastAPI uses Depends() — a lightweight, composable DI system declared entirely via type hints.
Depends() vs Express middleware vs NestJS providers
Section titled “Depends() vs Express middleware vs NestJS providers”// Express middleware (global side-effect style)function authMiddleware(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'Unauthorized' }); req.user = verifyToken(token); // attach to req next();}app.use(authMiddleware);
// NestJS — @Injectable() provider@Injectable()export class AuthService { getCurrentUser(token: string): User { ... }}// inject via constructorconstructor(private authService: AuthService) {}# FastAPI — Depends()from fastapi import FastAPI, Depends, HTTPException, Headerfrom typing import Optional
app = FastAPI()
# A dependency is just a callabledef get_current_user(authorization: Optional[str] = Header(None)): if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Unauthorized") token = authorization.removeprefix("Bearer ") return {"user_id": 1, "token": token} # return the value you need
# Inject with Depends() — runs before the handler@app.get("/profile")async def get_profile(current_user: dict = Depends(get_current_user)): return {"user": current_user}Reusable query parameters
Section titled “Reusable query parameters”A very common pattern is a shared “pagination” dependency that multiple routes share — the equivalent of a custom Express middleware or a NestJS PaginationDto.
// NestJS — shared DTOexport class PaginationDto { @IsOptional() @Type(() => Number) @IsInt() @Min(0) skip?: number = 0;
@IsOptional() @Type(() => Number) @IsInt() @Max(100) limit?: number = 10;}
@Get()findAll(@Query() pagination: PaginationDto) { ... }# FastAPI — pagination as a Dependsfrom fastapi import FastAPI, Depends
app = FastAPI()
class PaginationParams: def __init__(self, skip: int = 0, limit: int = 10): if skip < 0: raise HTTPException(status_code=400, detail="skip must be >= 0") if limit > 100: raise HTTPException(status_code=400, detail="limit must be <= 100") self.skip = skip self.limit = limit
@app.get("/items")async def list_items(pagination: PaginationParams = Depends()): return {"skip": pagination.skip, "limit": pagination.limit}
@app.get("/users")async def list_users(pagination: PaginationParams = Depends()): return {"skip": pagination.skip, "limit": pagination.limit}DB session dependency
Section titled “DB session dependency”The most important dependency in a real app is the database session. FastAPI’s Depends() handles the lifecycle — open, yield to the handler, close on completion. This mirrors the “unit of work” pattern NestJS handles via @InjectRepository().
// NestJS — TypeORM via constructor injection@Injectable()export class ItemsService { constructor( @InjectRepository(Item) private readonly itemRepo: Repository<Item>, ) {} async findAll() { return this.itemRepo.find(); }}# FastAPI — SQLAlchemy session via Dependsfrom fastapi import FastAPI, Dependsfrom sqlalchemy.orm import Sessionfrom app.database import SessionLocal
app = FastAPI()
# Generator dependency — yields session, cleans up afterdef get_db(): db = SessionLocal() try: yield db # ← handler receives this value finally: db.close() # ← always runs, even on exception
@app.get("/items/{item_id}")async def get_item(item_id: int, db: Session = Depends(get_db)): item = db.get(Item, item_id) if not item: raise HTTPException(status_code=404, detail="Item not found") return itemComposing dependencies
Section titled “Composing dependencies”Dependencies can depend on other dependencies — forming a chain. This is how you layer auth → permissions → resource fetching.
// NestJS guard chain@UseGuards(AuthGuard, RolesGuard)@Roles('admin')@Get(':id')findOne(@Param('id') id: string) { ... }# FastAPI — dependencies compose naturallyfrom fastapi import FastAPI, Depends
app = FastAPI()
def get_current_user(token: str = Depends(get_token)): return decode_token(token)
def require_admin(user: dict = Depends(get_current_user)): if user["role"] != "admin": raise HTTPException(status_code=403, detail="Admin only") return user
@app.delete("/items/{item_id}")async def delete_item(item_id: int, _: dict = Depends(require_admin)): return {"deleted": item_id}Run this locally — needs FastAPI + a server. Playground is skipped for this lesson because
Depends()requires the FastAPI request lifecycle to execute.