Part 2: Modularity¶
9. Modular Folder Structure for FastAPI¶
“FastAPI is flexible enough to let you do anything. But structure tells you what you should do.”
Why Modularity Matters More in Backend¶
In a React frontend, the biggest risks of poor structure are visual bugs, state bleed, or duplicated logic.
In a FastAPI backend, bad structure means:
- Route spaghetti
- Logic leakage
- Circular imports
- Untestable services
- Monolithic pain
Modular folder design in FastAPI creates cleanly separated APIs, testable service layers, and an architecture that evolves with your app.
This chapter walks you through how to build modular backends that scale—with clear folders, sharp imports, and minimal surprise.
What “Feature-Centric” Means in FastAPI¶
Just like the frontend, modular FastAPI apps group logic by feature, not just by file type.
Instead of:
app/
├── api/
├── services/
├── schemas/
You build:
app/
├── chatbot/
│ ├── api/
│ ├── services/
│ ├── schemas/
├── invoice/
│ ├── api/
│ ├── gpt_parser.py
│ └── schemas/
├── shared/
│ ├── ocr/
│ ├── gpt/
│ └── db/
Each feature module owns its API, logic, and schemas.
You gain:
- Isolation for debugging
- Parallel development
- Feature-based ownership
Example Modular FastAPI Folder Layout¶
app/
├── chatbot/
│ ├── api/ # Routes (chatbot endpoints)
│ ├── services/ # Business logic (streaming, tokenization)
│ ├── schemas/ # Request/response models
│ ├── utils/ # Optional: local GPT wrappers, guards
│ └── __tests__/ # Feature-scoped unit tests
├── invoice/
│ ├── api/
│ ├── services/
│ ├── schemas/
│ └── __tests__/
├── shared/
│ ├── gpt/ # GPT/OpenAI clients
│ ├── ocr/ # pytesseract, EasyOCR, image preprocessing
│ ├── db/ # DB engine, models, repositories
│ ├── auth/ # JWT validation, role guard utils
│ └── test_helpers/ # Mock clients, test fixtures
├── core/ # Global settings, constants, DI container
├── main.py # FastAPI app entry point
└── routers.py # Route inclusion and router setup
How to Organize Routes, Schemas, and Services¶
Routes → /api/
¶
Every feature should define its own routes in a dedicated file (or files) under api/
.
# chatbot/api/chat_routes.py
from fastapi import APIRouter
from ..services.chat_service import stream_response
router = APIRouter()
@router.post("/chat")
async def chat_handler(payload: ChatRequest):
return await stream_response(payload)
Then in routers.py
(or main.py
):
from chatbot.api import chat_routes
from invoice.api import invoice_routes
app.include_router(chat_routes.router, prefix="/chatbot")
app.include_router(invoice_routes.router, prefix="/invoice")
This modularizes routing logic without polluting main.py
.
Schemas → /schemas/
¶
Pydantic models for request/response validation should be:
- Inside each feature
- Named clearly (e.g.,
ChatRequest
,InvoiceUploadRequest
) - Not shared unless 100% generic
Keep your schemas tight and feature-specific—don’t overgeneralize prematurely.
Services → /services/
¶
This is your business logic layer. It should:
- Be importable in tests
- Never access
Request
/Response
objects directly - Contain all GPT, OCR, or parsing logic per feature
Example:
# invoice/services/gpt_parser.py
def extract_invoice_data(text: str) -> dict:
prompt = f"Extract fields from: {text}"
return call_gpt(prompt)
Avoid mixing API and service logic. Your services should feel pure.
Keeping Shared Logic Clean¶
Not everything belongs to a feature. Common patterns include:
Shared Concern | Location |
---|---|
GPT clients & config | shared/gpt/ |
OCR utilities | shared/ocr/ |
Database session, base models | shared/db/ |
Auth utilities | shared/auth/ |
Test fixtures | shared/test_helpers/ |
If two or more modules use it, pull it out of the feature folder.
Use shared/
to avoid duplication and prevent coupling.
Handling Circular Imports¶
Circular imports are a common pain in FastAPI modular design.
Problem:¶
chatbot/services/gpt.py
imports shared/gpt/client.py
but client.py
also imports schema from chatbot/schemas/
Solution:¶
- Never import feature-local files inside
shared/
- Move common schemas to
shared/schemas/
only if truly generic - Use Dependency Injection to decouple layers
Structuring Testable Services¶
Your services/
should be:
- Importable without side effects
- Independent from actual external APIs (thanks to wrappers/mocks)
- Tested inside
__tests__/
per feature
# chatbot/__tests__/test_streaming.py
from chatbot.services.chat_service import stream_response
def test_stream_yields_valid_chunks():
payload = ChatRequest(message="Hi")
chunks = list(stream_response(payload))
assert len(chunks) > 0
Use shared/test_helpers/
to store:
- Mock GPT/OpenAI clients
- Mock DB responses
- Test images for OCR
Dependency Injection and Inversion¶
FastAPI’s Depends
is your secret weapon for modular testable code.
Example:
def get_vector_db():
return SupabaseVectorStore()
@router.post("/embed")
def embed_doc(input: DocInput, db = Depends(get_vector_db)):
return db.add(input)
In tests:
def test_embed(monkeypatch):
monkeypatch.setattr("shared.vectorstore.get_vector_db", lambda: MockVectorDB())
This keeps services decoupled and mockable, a key modularity goal.
Summary: Modular FastAPI Guidelines¶
Principle | Practice |
---|---|
Feature-based foldering | Each feature gets api/ , services/ , schemas/ |
Keep routes thin | All logic moves to services |
Local schemas | Use per-feature Pydantic models |
Shared logic lives in shared/ |
Only extract when multiple features need it |
Avoid circular imports | Never import upwards; use DI patterns |
Local tests per module | Use __tests__/ to scope and scale testing |