FastAPI Folder Structure: How to scale from One File to Thirty Modules

amai1 pts0 comments

FastAPI Folder Structure: How to Organize a Project From One File to Thirty Modules | FastroAI Blog<br>v3 launch: Solopreneur $299 $399 with code LAUNCHTODAY claim →

The layout you reach for grows with the app: one file, then by file type for a single domain, then by feature once domains pile up.

Everyone has opinions about FastAPI folder structure and most of them are vibes; the part that matters you usually decide on day one, when the app is four routes and the layout feels like the least important thing in the world. Then it’s month six, twenty domains import each other, and moving one file means a find-and-replace across the whole repo and a small prayer.

The best-practices post already made the call I’d defend hardest, organize by feature and not by file type, and I won’t relitigate it. This is everything around that: the other layouts and why they exist, what goes inside a feature folder once you’ve got one, the two rules that decide whether this ages well or becomes spaghetti with tidier names, and where clean architecture and DDD fit if you’ve heard the words and wondered whether you’re skipping homework. Roughly in the order you hit them.

One file

Every app starts as one main.py, and plenty should stay there. A demo, an internal tool with five endpoints, a throwaway: splitting that across eight folders is navigating a tree to read what fit on one screen.

You’ve outgrown it when you start scrolling to find a route, or when two people keep landing on the same file in every pull request. Then you split; how you split is the rest of this.

By file type

The first split most people reach for groups files by what they are:

app/<br>├── routers/ # users.py, products.py, orders.py<br>├── models/ # users.py, products.py, orders.py<br>├── schemas/ # users.py, products.py, orders.py<br>└── main.py<br>For one small domain this is not only fine but better. It’s what the official tutorial shows, it reads cleanly while routers/ has three files in it, and a microservice that owns a single concern can live like this forever.

This starts to hurt with more domains. By a dozen features, routers/ is a junk drawer of twenty unrelated files, every feature is smeared across three directories, and touching “users” means opening routers/users.py, models/users.py, and schemas/users.py in three different corners of the tree. The best-practices post has the full autopsy on why that retrofit hurts, so I’ll point you there. The short version: you’ve filed your code by Python category, but you navigate it by product domain, and those two never line up.

Filed by Python category, the "user" feature is smeared across three folders; filed by feature, it's one folder you read top to bottom.

By feature

Put everything about a domain in one folder, and put the cross-cutting machinery in a package the domains import from:

backend/src/<br>├── modules/<br>│ ├── user/<br>│ │ ├── router.py # HTTP edge: paths, status codes, dependencies<br>│ │ ├── schemas.py # request/response contracts (Pydantic)<br>│ │ ├── models.py # storage shape (SQLAlchemy)<br>│ │ ├── service.py # business logic, no HTTP<br>│ │ └── crud.py # data access, no business rules<br>│ └── billing/<br>│ └── ...<br>├── infrastructure/<br>│ ├── config/<br>│ ├── database/<br>│ ├── auth/<br>│ ├── cache/<br>│ ├── storage/<br>│ └── observability/<br>├── workers/ # long-running background processes<br>├── api.py # mounts every module's router<br>└── main.py # app object and lifespan<br>Now a feature is a folder; you open it, read the whole thing from HTTP down to the database, and you can move it or delete it without searching for stuff all over the place. New people find code by domain, which is how they already think about the product. fastro.ai runs this in production, and I’d reach for it on day one with two endpoints if I know the project will grow, because it’s the most expensive thing here to change later and free to adopt now.

It has a name, and the name is worth knowing because it tells people what you’re doing: a modular monolith . One thing you deploy as a unit, carved up inside into modules with real separation between them. You get code organized like microservices without paying to operate microservices, which stays the right call for a long time.

Drawing that tree is one thing; whether it lasts comes down to two others: what’s inside a module, and how modules are allowed to touch each other.

Inside a module

Five files. The split is what lets the code do anything beyond answer an HTTP request.

The router is the boring HTTP edge, the service holds what the feature does with no HTTP in it, and crud is queries only, so the same logic runs from a route, a worker, or a test.

The router is the HTTP edge and it should be boring: paths, dependencies, status codes, hand off. The moment it grows an if about a business rule, you’ve buried logic somewhere only an HTTP request can reach it.

That logic belongs in the service , the one file in here I care about: it holds what the feature does, and it doesn’t know HTTP exists: no Request, no...

file feature http folder users from

Related Articles