As a FastAPI application grows, putting everything inside the route handlers quickly becomes difficult to maintain.
A better approach is Layered Architecture, where each layer has a clear responsibility.
The basic structure looks like this:
app/├── routers/├── schemas/├── services/├── repositories/├── models/├── core/└── db/
The request flow is:
Client↓Router↓Schema↓Service↓Repository↓Database
The Router handles HTTP-related concerns.
It receives the request, calls the appropriate service, and returns the response.
@router.post("/orders")def create_order(data: OrderCreate,service: OrderService = Depends(get_order_service)):return service.create_order(data)
The router should not contain complex business logic.
Schemas define the structure of incoming and outgoing data.
FastAPI commonly uses Pydantic for validation:
class OrderCreate(BaseModel):product_id: intquantity: int
If the client sends invalid data, FastAPI can automatically return a validation error.
The Service contains business logic.
For example:
class OrderService:def create_order(self, data):if data.quantity <= 0:raise InvalidQuantityError()return self.repository.create(data)
This is where rules such as pricing, permissions, order validation, or payment logic usually belong.
The Repository is responsible for database operations.
class OrderRepository:def create(self, data):order = Order(product_id=data.product_id,quantity=data.quantity)self.db.add(order)self.db.commit()return order
The service should focus on what the application needs, while the repository handles how data is stored or retrieved.
Models represent database entities.
For example:
class Order(Base):__tablename__ = "orders"id = Column(Integer, primary_key=True)product_id = Column(Integer)quantity = Column(Integer)
With SQLAlchemy, models map application objects to database tables.
The biggest benefit is separation of responsibilities.
Without layers:
Router├── validation├── business logic├── SQL queries├── authentication└── response formatting
The route quickly becomes difficult to test and maintain.
With layers:
Router↓Service↓Repository↓Database
Each layer has a specific job.
This also makes testing easier. For example, you can test the service layer without making real database queries by mocking the repository.
These are not necessarily competing approaches.
Layered architecture organizes code by responsibility:
routers/services/repositories/models/schemas/
Feature-based architecture organizes code by business feature:
modules/├── users/├── orders/├── products/└── payments/
For a large FastAPI application, they can be combined:
modules/├── users/│ ├── router.py│ ├── schema.py│ ├── service.py│ └── repository.py│└── orders/├── router.py├── schema.py├── service.py└── repository.py
This gives you both clear layers and clear feature boundaries.
Layered Architecture separates responsibilities, making a FastAPI application easier to understand, test, and maintain.
A good interview answer is:
“I use layered architecture to separate HTTP handling, validation, business logic, and data access. Routers handle HTTP concerns, services contain business rules, and repositories handle database operations. For larger systems, I usually combine this with feature-based organization.”