The Anatomy of a Clean Backend: From Spaghetti to Architecture
When you first start building an API with FastAPI (or any framework), the goal is simple: Make it work. But as your application grows from a weekend project into a serious SaaS platform, the “simple” way becomes a nightmare. Let’s trace the evolution of a feature. Say, “Registering a User” to understand why we end up with the Controller → Service → Repository pattern.Phase 1: The “Do It All” Controller (The Junior Phase)
In the beginning, you put everything in one place. The Controller (the API route) handles the HTTP request, validates the email, calculates the password hash, connects to the database, and executes SQL. The Code:- It’s Un-testable: You can’t test the email validation logic without spinning up a real database.
- No Reuse: If you want to create a user from a CLI command or a background script, you have to copy-paste this code.
- Connection Chaos: If you call another function that also opens a connection, you now have two open connections for one request.
Phase 2: The Separation of Duties (The Intermediate Phase)
You realize the Controller is doing too much. You decide to split the code into three layers.- Controller: “I only handle HTTP (JSON in, JSON out).”
- Service: “I only handle Logic (Rules, calculations, decisions).”
- Repository: “I only handle Data (SQL, Database).”
- Controller = The Waiter. They take the order (Request) and bring food (Response). They don’t cook.
- Service = The Chef. They take ingredients and cook the meal. They don’t serve tables.
- Repository = The Supplier. They provide the raw ingredients (Data). They don’t cook.
- The “Mocking” Problem: If you want to test the Chef (Service), you are forced to bring in the real Supplier (Repository). You can’t just say, “Pretend we have ingredients.” The Service is hard-coded to talk to the real database.
Phase 3: Dependency Injection (The “Pro” Phase)
This is where we arrive at your solution. Instead of the Service creating the Repository, we give (inject) the Repository to the Service. The Concept: The Chef (Service) says: “I don’t care where the ingredients come from. Just hand me a valid Supplier (Repository) when I start my shift.” This is Dependency Injection (DI). We need a “Manager” (FastAPI/dependencies.py) to set everything up before the shift starts (before the Request is handled).
Step 1: The Repository (The Supplier)
It needs a database connection to work. It doesn’t open it; it asks for it.
Summary: Why did we do all this?
It looks like more code, but here is the payoff:-
The “Lego” Effect (Decoupling):
Because the Service doesn’t create the Repository, you can swap the Repository out.
- Today: You use
PostgresRepository. - Tomorrow: You switch to
MongoRepository. - Result: You change the wiring in
dependencies.py, and the Service code doesn’t change at all.
- Today: You use
-
Testing is a Breeze:
When testing the Service, you don’t need a database. You can inject a
FakeRepositorythat returns dummy data in memory. -
Transaction Safety (The “Unit of Work”):
Because the DB connection is created at the very top (in
dependencies.py) and passed down, every repository uses the exact same connection for that request.- If you save a User and then save a Payment, and the Payment fails, you can rollback everything because they share the same session.
The Final mental model
- Controller: The Interface (HTTP).
- Service: The Brain (Logic).
- Repository: The Storage (SQL).
- Dependency Injection: The Assembly Line that puts them together properly so they don’t depend on each other hard-codedly.