FastAPI project structure
If you are asked “what are the two types of FastAPI project structure”, the honest answer is that the official documentation does not define two types. That framing is community shorthand. The words monolithic, flat, layered and feature-based appear nowhere in the docs, and neither does the “5-10 endpoints” threshold people quote alongside them.
What the docs actually contain is one implicit default and one documented layout:
| Form | Where in the docs | Status |
|---|---|---|
Single main.py |
Tutorial, from First Steps | Never named as a structure |
app/ package + APIRouter |
Bigger Applications | The one canonical layout |
Everything past that — services/, repositories/, domain/, hexagonal,
feature slices — is convention with no official backing. Saying so is a better
answer than picking one of two invented categories, because it shows you know
where the framework stops and architecture begins.
The single file
Every tutorial example from First Steps through SQL Databases lives in one file. The docs never call it a structure or give it rules:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}The Bigger Applications page opens by observing that a real application rarely fits in a single file. So this is the thing you outgrow, not a pattern you choose.
The documented layout
.
├── app
│ ├── __init__.py
│ ├── main.py
│ ├── dependencies.py
│ ├── routers
│ │ ├── __init__.py
│ │ ├── items.py
│ │ └── users.py
│ └── internal
│ ├── __init__.py
│ └── admin.pyWhat it is organised by
Read this carefully, because it is the part people get wrong: HTTP
resource (users, items), plus a visibility split between public routers/
and internal/. It is not organised by architectural layer. There is no
services/, no repositories/, no schemas/ versus models/ split. Shared
dependencies sit in one dependencies.py at the package root.
Every directory carries an __init__.py. That is what makes app a package
and app.routers.items an importable submodule.
APIRouter is a mini FastAPI
The docs describe APIRouter as supporting all the same options as the FastAPI
class — same parameters, responses, dependencies and tags. If you know Flask,
it is the equivalent of a Blueprint.
Hoist the repeated configuration onto the router rather than onto every path operation:
from fastapi import APIRouter, Depends, HTTPException
from ..dependencies import get_token_header
router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
@router.get("/{item_id}")
async def read_item(item_id: str):
if item_id not in fake_items_db:
raise HTTPException(404, "Item not found")
return {"item_id": item_id}The four router parameters
| Parameter | What it does |
|---|---|
prefix |
Prepended to every path. Leading /, no trailing / |
tags |
Applied to all operations; groups them in the OpenAPI docs |
responses |
Extra documented responses merged into each operation |
dependencies |
Run on every request to any operation in the router |
Per-operation additions still compose: a path operation that adds
tags=["custom"] ends up with both items and custom.
dependencies on the router is the idiomatic way to require auth for a whole
group without repeating it. Order of execution: router dependencies, then
decorator dependencies, then normal parameter dependencies — and as with
decorator dependencies, no value is passed into the function.
Relative imports are where people trip
The docs give this a section of its own, which tells you how often it bites.
From app/routers/items.py:
| Prefix | Resolves to | Works |
|---|---|---|
.dependencies |
app/routers/dependencies.py |
no, absent |
..dependencies |
app/dependencies.py |
yes |
...dependencies |
above app/ |
no, no parent |
One dot is “this module’s package”, two is “the parent package”, three is a level that does not exist here.
The main module
from fastapi import Depends, FastAPI
from .dependencies import get_query_token
from .internal import admin
from .routers import items, users
app = FastAPI(dependencies=[Depends(get_query_token)])
app.include_router(users.router)
app.include_router(items.router)
app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
responses={418: {"description": "I'm a teapot"}},
)Gotcha: import the submodule, not the
routervariable. Bothitems.pyandusers.pyname their routerrouter, sofrom .routers.items import routerfollowed by the same line foruserssilently overwrites the first.
Dependencies passed to FastAPI() are global and combine with each router’s own.
Configuring at inclusion time
The admin router in the docs models a real constraint: it is shared across
several projects, so you cannot edit it to add a prefix or auth. Passing
prefix, tags, dependencies and responses to include_router() applies
them without mutating the original, so another project can include the same
router behind a different authentication scheme.
Two further patterns, both flagged as advanced:
- The same router included twice under different prefixes — how you serve
an API at both
/api/v1and/api/latest. - Nested routers —
router.include_router(other), which is the mechanism behind the commonapp/api/v1/__init__.pyaggregator.
Two technical points worth repeating in an interview, because they sound wrong until you know them:
- Routers are not mounted and are not isolated. That is deliberate — it is what keeps their operations in the OpenAPI schema. FastAPI keeps the original routers active and merges prefixes, dependencies, tags and responses when handling requests and generating OpenAPI.
- Including routers adds no per-request overhead; the docs say so outright.
Do not mutate router.routes after inclusion — it is a lower-level tree that
may hold both route definitions and included routers, not a flat list.
Pointing the CLI at it
With app living in app/main.py, declare the entrypoint once:
[tool.fastapi]
entrypoint = "app.main:app"Equivalent to from app.main import app. You can pass the path manually
(fastapi dev app/main.py), but then you must remember it every time and other
tooling may not find the app.
What the docs deliberately leave out
There is no official guidance on where business logic lives, where persistence lives, whether Pydantic schemas are separated from ORM models, how to slice by domain rather than resource, or how to lay out settings and migrations.
The closest thing to an opinionated end-to-end answer is the Full Stack FastAPI Template, which the docs present as a customizable starting point rather than a prescription.
So the practical position: take app/ + routers/ + dependencies.py +
include_router() as the sanctioned baseline, and treat any layering on top of
it as your own architectural decision. The two community references people cite
are Netflix’s dispatch (layered) and zhanymkanov/fastapi-best-practices
(domain-based). See Architecture & design
for the reasoning behind those choices.
Related
Interview angle 5
- “What are the two types of FastAPI project structure?” - the premise is community shorthand, not documentation. The docs show a single-file default and exactly one multi-file layout: an
app/package withrouters/, a shareddependencies.py, andinclude_router(). Flat versus layered is a distinction teams invented; saying so is stronger than picking one. - “How do you split a FastAPI app across files?” -
APIRouter, which the docs call a miniFastAPIand which maps to a Flask Blueprint. Hoistprefix,tags,responsesanddependenciesonto the router so they are declared once rather than per operation. - “How do you require auth for a whole group of endpoints?” -
dependencies=[Depends(...)]on theAPIRouter, or oninclude_router()when you do not own the router. Router dependencies run before decorator dependencies, and neither passes a value into the path operation. - “Does including a router cost anything at request time?” - no. Routers are not mounted and not isolated; FastAPI keeps the original path operations active and merges the metadata, which is exactly why they still appear in the OpenAPI schema.
- “Where do services and repositories go?” - nowhere the docs specify. That is the point at which FastAPI stops having an opinion and you are making an architecture decision, so justify it by team size and testing needs rather than by citing the framework.