FastAPI Project Structure: Best Practices for Medium Apps
When a FastAPI codebase grows beyond a handful of endpoints, the folder layout can feel like the difference between a smooth sprint and a clunky crawl. A sensible structure not only tames complexity, it also eases onboarding and keeps the debug loop tight.
Why a Thoughtful Structure Matters
FastAPI shines in its flexibility, but that freedom can become a double‑edged sword. Without conventions, developers start sprinkling routes, schemas, and utilities everywhere, and soon the project looks like a tangled web. A clear hierarchy gives each piece a purpose, reduces merge conflicts, and makes automated testing more straightforward.
At the medium‑scale level—think a dozen services or a few thousand lines of code—over‑engineering is just as harmful as under‑planning. The goal is to strike a balance: enough scaffolding to stay organized, yet light enough to avoid boilerplate fatigue.
Core Directory Layout
Here’s a practical layout that has worked for several teams. Feel free to tweak names, but keep the overall intent intact.
- app/ – the main package housing all source files.
- app/api/ – routers, endpoint functions, and versioned sub‑folders.
- app/core/ – configuration, security helpers, and shared utilities.
- app/models/ – SQLAlchemy or Pydantic models representing database tables.
- app/crud/ – data‑access functions that isolate ORM logic from business code.
- app/schemas/ – request/response validation objects.
- app/services/ – higher‑level business logic that coordinates multiple crud calls.
- app/main.py – the entry point creating the FastAPI app and including routers.
- tests/ – pytest suites mirroring the app structure for unit and integration checks.
app/api
Each resource gets its own module, for example users.py or items.py. Inside, define a router = APIRouter() and attach path operations. If you need versioning, nest a v1/ folder under api/ and import routers accordingly.
app/core
Settings live here, typically using pydantic.BaseSettings. A security.py file can host OAuth2 helpers, while dependencies.py gathers reusable Depends objects.
app/models & app/crud
Separate pure ORM definitions from the functions that read or write them. This split makes mocking in tests painless, and it keeps route handlers thin.
Organizing Routes and Controllers
Never let a single router file balloon beyond a few dozen lines. When a module starts to feel crowded, carve out a sub‑module. For example, split users.py into users/auth.py and users/profile.py. Then, in app/main.py, include the routers with clear prefixes:
app.include_router(user_auth.router, prefix="/auth", tags=["auth"])app.include_router(user_profile.router, prefix="/users", tags=["users"])
This keeps the generated OpenAPI docs tidy and conveys intent at a glance.
Dependency Injection and Common Utilities
FastAPI’s Depends system is a powerful way to share resources without globals. Store reusable dependencies in app/core/dependencies.py:
def get_db()– yields a session scoped to the request.def get_current_user()– extracts and validates a JWT token.def pagination_params()– parsesskipandlimitquery parameters.
Import these where needed; the route functions stay focused on input validation and delegating to services.
Managing Settings and Environment
Configuration should never be hard‑coded. A typical settings.py might look like:
class Settings(BaseSettings):database_url: str = "sqlite:///./test.db"
secret_key: str
debug: bool = False
class Config:
env_file = ".env"
Load a singleton instance at startup and inject it with Depends(get_settings). This pattern keeps local development, staging, and production environments isolated without changing code.
Testing Strategy Aligned with Structure
Unit tests target the smallest pieces—crud functions, services, and utilities. Integration tests spin up a TestClient against the full app, hitting the routers.
Mirror the app/ layout inside tests/ so that a test for app/api/users.py lives in tests/api/test_users.py. This visual correspondence cuts the time spent hunting files.
- Use fixtures to provide a fresh database session.
- Mock external services (e.g., email) with
unittest.mockorhttpx.AsyncClientin isolation mode. - Run the test suite in parallel (via
pytest-xdist) to keep feedback loops short.
When a test fails, the path from the failing endpoint back to the underlying service is usually only two imports away—thanks to the clear separation you enforced earlier.
Version Control Tips That Complement the Layout
Commit at the feature level, not at the file level. A change that adds a new endpoint often touches api/, schemas/, and services/. Group those changes in one pull request; reviewers get the full context and can spot mismatches early.
Also, add a .pre-commit hook that runs ruff and isort. Consistent formatting reinforces the mental map of the project.
Conclusion
Medium‑sized FastAPI applications thrive on a structure that separates concerns without imposing unnecessary bureaucracy. By carving out dedicated packages for routers, models, CRUD, and utilities, you keep the codebase approachable, testable, and ready for the next growth spurt.