Reusable Components and ServicesΒΆ
π― What Youβll LearnΒΆ
- Why separating business logic from routing improves maintainability
- How to build service functions that encapsulate core logic
- How to inject services using FastAPIβs
Dependssystem - How to structure your project for reuse and testing
π§ Why Use Services?ΒΆ
In a real-world API, routes should focus on:
- HTTP concerns (status codes, request/response)
- Dependency injection
- Input/output validation
Business logic β like querying the database, applying rules, or transforming data β should live in service functions.
Benefits:
- β Easier to test
- β Easier to reuse across routes
- β Easier to refactor and extend
ποΈ Suggested Project StructureΒΆ
task_manager/
βββ main.py
βββ db.py
βββ models/
β βββ task.py
βββ routers/
β βββ tasks.py
βββ services/
β βββ task_service.py
π§± Step 1: Move Business Logic to a ServiceΒΆ
π services/task_service.py
from sqlmodel import Session, select
from models.task import Task
from fastapi import HTTPException
def get_task_by_id(task_id: int, session: Session) -> Task:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
def list_tasks(session: Session) -> list[Task]:
return session.exec(select(Task)).all()
def create_task(task: Task, session: Session) -> Task:
session.add(task)
session.commit()
session.refresh(task)
return task
def update_task(task_id: int, updated: Task, session: Session) -> Task:
task = get_task_by_id(task_id, session)
task.title = updated.title
task.description = updated.description
task.completed = updated.completed
session.commit()
session.refresh(task)
return task
def delete_task(task_id: int, session: Session) -> None:
task = get_task_by_id(task_id, session)
session.delete(task)
session.commit()
π Step 2: Use Services in Your RouterΒΆ
π routers/tasks.py
from fastapi import APIRouter, Depends
from sqlmodel import Session
from db import get_session
from models.task import Task
from services import task_service
router = APIRouter(prefix="/tasks", tags=["Tasks"])
@router.get("/")
def get_all(session: Session = Depends(get_session)):
return task_service.list_tasks(session)
@router.get("/{task_id}")
def get_one(task_id: int, session: Session = Depends(get_session)):
return task_service.get_task_by_id(task_id, session)
@router.post("/", status_code=201)
def create(task: Task, session: Session = Depends(get_session)):
return task_service.create_task(task, session)
@router.put("/{task_id}")
def update(task_id: int, updated: Task, session: Session = Depends(get_session)):
return task_service.update_task(task_id, updated, session)
@router.delete("/{task_id}", status_code=204)
def delete(task_id: int, session: Session = Depends(get_session)):
task_service.delete_task(task_id, session)
π§ Best PracticesΒΆ
- Keep service functions pure and focused on business logic
- Avoid using
HTTPExceptionin services unless itβs part of the domain logic - Use
Dependsto inject shared resources like sessions or authenticated users - Write unit tests for services independently from routes
π§ͺ Practice ChallengeΒΆ
Create a new service:
user_service.pywith functions to create and fetch users- Inject it into a
users.pyrouter - Use
Depends(get_session)to pass the session into service functions