Problem
The current project uses a flat structure with top-level folders for routes/, models/, schemas/, services/, and databases/. While functional for small applications, this structure has several limitations:
Pain Points:
- Scalability Issues: As features grow, flat structure becomes difficult to navigate
- Import Complexity: Long import paths like
from routes.player_route import api_router
- Naming Redundancy: Files named
player_route.py, player_model.py, player_schema.py repeat context
- Non-idiomatic: Doesn't follow [FastAPI's recommended "bigger applications" pattern](https://fastapi.tiangolo.com/tutorial/bigger-applications/)
- Testing Confusion: Test imports are verbose and unclear about package boundaries
- Deployment Ambiguity: No clear application entry point package
- Future Feature Isolation: Difficult to add new features without polluting global namespace
Current Structure Problems:
routes/player_route.py # ❌ Redundant suffix
models/player_model.py # ❌ Redundant suffix
schemas/player_schema.py # ❌ Redundant suffix
services/player_service.py # ❌ Redundant suffix
main.py # ❌ Not in app package
This makes the codebase harder to maintain as it grows beyond a simple proof-of-concept.
Proposed Solution
Migrate to FastAPI's "Bigger Applications" architecture pattern by:
- Grouping all application logic under a single
app/ package
- Organizing by layer (routers, models, schemas, services, databases) within
app/
- Removing redundant suffixes from filenames (folder names provide context)
- Standardizing router naming to just
router instead of api_router
- Creating clear package boundaries for better imports and testing
Benefits:
- ✅ Cleaner imports:
from app.routers import player vs from routes.player_route import api_router
- ✅ Better scalability: Easy to add new features without namespace pollution
- ✅ Industry standard: Follows FastAPI best practices and community conventions
- ✅ Clearer ownership:
app/ package clearly defines application boundary
- ✅ Simpler filenames:
player.py instead of player_route.py
- ✅ Future-proof: Ready for microservices, plugins, or monorepo structure
- ✅ Better IDE support: Package structure improves autocomplete and navigation
Target Structure:
app/
├── __init__.py # Application package
├── main.py # FastAPI app initialization
├── dependencies.py # Shared dependencies (future)
├── routers/
│ ├── __init__.py
│ ├── health.py # Health check endpoints
│ └── player.py # Player CRUD endpoints
├── models/
│ ├── __init__.py
│ └── player.py # Pydantic models
├── schemas/
│ ├── __init__.py
│ └── player.py # SQLAlchemy ORM models
├── services/
│ ├── __init__.py
│ └── player.py # Business logic
└── databases/
├── __init__.py
└── player.py # Database setup & sessions
Suggested Approach
Phase 1: Create Application Package Structure
1.1 Create app/ Package
mkdir -p app/routers app/models app/schemas app/services app/databases
touch app/__init__.py
touch app/routers/__init__.py
touch app/models/__init__.py
touch app/schemas/__init__.py
touch app/services/__init__.py
touch app/databases/__init__.py
1.2 Create app/__init__.py
"""
FastAPI application package.
This package contains all application logic organized by layer:
- routers: API endpoint definitions
- models: Pydantic validation models
- schemas: SQLAlchemy ORM models
- services: Business logic layer
- databases: Database configuration and sessions
"""
__version__ = "1.0.0"
Phase 2: Migrate Core Application Files
2.1 Move and Update main.py
Move: main.py → app/main.py
Update imports and router registration:
"""
Main application module for the FastAPI RESTful API.
- Sets up the FastAPI app with metadata (title, description, version).
- Defines the lifespan event handler for app startup/shutdown logging.
- Includes API routers for player and health endpoints.
This serves as the entry point for running the API server.
"""
from contextlib import asynccontextmanager
import logging
from typing import AsyncIterator
from fastapi import FastAPI
from app.routers import player, health # ✅ Updated import
# https://github.com/encode/uvicorn/issues/562
UVICORN_LOGGER = "uvicorn.error"
logger = logging.getLogger(UVICORN_LOGGER)
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
"""Lifespan event handler for FastAPI."""
logger.info("Lifespan event handler execution complete.")
yield
app = FastAPI(
lifespan=lifespan,
title="python-samples-fastapi-restful",
description="🧪 Proof of Concept for a RESTful API made with Python 3 and FastAPI",
version="1.0.0",
)
app.include_router(player.router) # ✅ Updated router name
app.include_router(health.router) # ✅ Updated router name
Phase 3: Migrate and Rename Router Modules
3.1 Migrate Health Router
Move: routes/health_route.py → app/routers/health.py
Update router variable name:
"""Health check endpoints."""
from fastapi import APIRouter
router = APIRouter( # ✅ Changed from api_router
prefix="/health",
tags=["health"],
)
@router.get("")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy"}
3.2 Migrate Player Router
Move: routes/player_route.py → app/routers/player.py
Update imports and router name:
"""Player CRUD endpoints."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.databases.player import generate_async_session # ✅ Updated import
from app.services import player as player_service # ✅ Updated import
from app.models.player import PlayerModel # ✅ Updated import
router = APIRouter( # ✅ Changed from api_router
prefix="/players",
tags=["players"],
)
@router.get("/{player_id}")
async def get_player(
player_id: int,
session: AsyncSession = Depends(generate_async_session)
):
"""Get player by ID."""
# Implementation...
Phase 4: Migrate Data Layer Modules
4.1 Migrate Database Configuration
Move: databases/player_database.py → app/databases/player.py
No code changes needed, just update docstring:
"""
Database setup and session management for async SQLAlchemy with SQLite.
Part of the app.databases layer providing database connectivity.
"""
# ... rest of file unchanged
4.2 Migrate SQLAlchemy ORM Models
Move: schemas/player_schema.py → app/schemas/player.py
Update imports:
"""
SQLAlchemy ORM model for the Player database table.
Defines the schema and columns corresponding to football player attributes.
"""
from sqlalchemy import Column, String, Integer, Boolean
from app.databases.player import Base # ✅ Updated import
class Player(Base):
"""SQLAlchemy schema describing a database table of football players."""
__tablename__ = "players"
# ... rest unchanged
4.3 Migrate Pydantic Models
Move: models/player_model.py → app/models/player.py
No import changes needed (uses only Pydantic):
"""
Pydantic models defining the data schema for football players.
Part of the app.models layer for API validation and serialization.
"""
# ... rest of file unchanged
4.4 Migrate Service Layer
Move: services/player_service.py → app/services/player.py
Update imports:
"""
Business logic for player operations.
Part of the app.services layer handling CRUD operations and business rules.
"""
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.player import Player # ✅ Updated import
from app.models.player import PlayerModel # ✅ Updated import
# ... rest of implementation
Phase 5: Update Configuration Files
5.1 Update Dockerfile
Change the uvicorn command:
# Before
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"]
# After
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "9000"]
5.2 Update compose.yaml
Update the command:
services:
api:
# ...
command: uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
5.3 Update pyproject.toml (if using)
Update any references to main module:
[tool.pytest.ini_options]
pythonpath = "."
testpaths = ["tests"]
Phase 6: Update Tests
6.1 Update tests/conftest.py
Update imports:
"""Test configuration and fixtures."""
import pytest
from app.main import app # ✅ Updated import
from app.databases.player import Base, async_engine # ✅ Updated import
@pytest.fixture
def test_app():
"""Provide FastAPI test client."""
return app
6.2 Update tests/test_main.py
Update all imports:
"""Integration tests for the FastAPI application."""
from fastapi.testclient import TestClient
from app.main import app # ✅ Updated import
from app.models.player import PlayerModel # ✅ Updated import
client = TestClient(app)
def test_health_check():
"""Test health endpoint."""
response = client.get("/health")
assert response.status_code == 200
6.3 Update tests/player_stub.py
Update imports:
"""Test fixtures and stubs for player data."""
from app.models.player import PlayerModel # ✅ Updated import
from app.schemas.player import Player # ✅ Updated import
def create_test_player() -> PlayerModel:
"""Create a test player instance."""
# ... implementation
Phase 7: Cleanup and Documentation
7.1 Remove Old Directories
rm -rf routes/
rm -rf models/
rm -rf schemas/
rm -rf services/
rm -rf databases/
rm main.py # Now in app/main.py
7.2 Update README.md
app/
├── main.py # FastAPI application initialization
├── routers/ # API endpoint definitions
├── models/ # Pydantic validation models
├── schemas/ # SQLAlchemy ORM models
├── services/ # Business logic layer
└── databases/ # Database configuration
# Development
uvicorn app.main:app --reload
# Production
uvicorn app.main:app --host 0.0.0.0 --port 9000
7.3 Update .gitignore (if needed)
Ensure __pycache__ in app/ is ignored:
# Python
__pycache__/
*.py[cod]
*$py.class
app/__pycache__/
Phase 8: Optional Enhancements
8.1 Add app/dependencies.py (Future Use)
"""
Shared FastAPI dependencies.
Common dependency functions used across multiple routers.
"""
from typing import Annotated
from fastapi import Depends, Header, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.databases.player import generate_async_session
# Example: Database session dependency with type annotation
AsyncSessionDep = Annotated[AsyncSession, Depends(generate_async_session)]
# Example: Common auth dependency (future)
# async def get_current_user(token: str = Header(...)) -> User:
# ...
8.2 Update Router to Use Dependencies
"""Player CRUD endpoints."""
from fastapi import APIRouter, HTTPException
from app.dependencies import AsyncSessionDep # ✅ Cleaner dependency injection
from app.services import player as player_service
from app.models.player import PlayerModel
router = APIRouter(prefix="/players", tags=["players"])
@router.get("/{player_id}")
async def get_player(player_id: int, session: AsyncSessionDep):
"""Get player by ID."""
# Implementation...
Phase 9: Integration with Other Issues
9.1 Alembic Configuration
If implementing Alembic migrations (see related issue), update alembic/env.py:
from app.databases.player import Base # ✅ Updated import
from app.schemas.player import Player # ✅ Updated import
9.2 SQLModel Integration
If implementing SQLModel (see related issue), the structure remains the same:
app/
├── models/
│ └── player.py # SQLModel classes (unified ORM + Pydantic)
The schemas/ folder could be removed entirely with SQLModel since it unifies both layers.
Acceptance Criteria
Structure & Organization
Core Application
Code Migration
Imports & Dependencies
Testing
Deployment & Infrastructure
Documentation
Integration with Related Issues
Quality Assurance
References
Migration Strategy
Risk Assessment
Low Risk:
- Pure code reorganization
- No business logic changes
- Easily reversible via Git
Potential Issues:
- Import errors if not thorough
- Test failures if imports missed
- Docker build issues if paths wrong
Rollback Plan
- Keep original structure in Git history
- Create feature branch:
feature/restructure-app-package
- Test thoroughly before merging to main
- If issues arise:
git revert is straightforward
Testing Strategy
- After each migration phase: Run
pytest to catch import errors early
- Before Docker update: Test locally with
uvicorn app.main:app
- After Docker update: Test container build and runtime
- Final verification: Run full test suite + manual API testing
Timeline Estimate
- Phase 1-2: 30 minutes (setup and core files)
- Phase 3-4: 1 hour (migrate all modules)
- Phase 5-6: 30 minutes (update config and tests)
- Phase 7-8: 30 minutes (cleanup and docs)
- Total: ~2.5 hours for careful migration
Communication
Before starting:
Problem
The current project uses a flat structure with top-level folders for
routes/,models/,schemas/,services/, anddatabases/. While functional for small applications, this structure has several limitations:Pain Points:
from routes.player_route import api_routerplayer_route.py,player_model.py,player_schema.pyrepeat contextCurrent Structure Problems:
This makes the codebase harder to maintain as it grows beyond a simple proof-of-concept.
Proposed Solution
Migrate to FastAPI's "Bigger Applications" architecture pattern by:
app/packageapp/routerinstead ofapi_routerBenefits:
from app.routers import playervsfrom routes.player_route import api_routerapp/package clearly defines application boundaryplayer.pyinstead ofplayer_route.pyTarget Structure:
Suggested Approach
Phase 1: Create Application Package Structure
1.1 Create
app/Package1.2 Create
app/__init__.pyPhase 2: Migrate Core Application Files
2.1 Move and Update
main.pyMove:
main.py→app/main.pyUpdate imports and router registration:
Phase 3: Migrate and Rename Router Modules
3.1 Migrate Health Router
Move:
routes/health_route.py→app/routers/health.pyUpdate router variable name:
3.2 Migrate Player Router
Move:
routes/player_route.py→app/routers/player.pyUpdate imports and router name:
Phase 4: Migrate Data Layer Modules
4.1 Migrate Database Configuration
Move:
databases/player_database.py→app/databases/player.pyNo code changes needed, just update docstring:
4.2 Migrate SQLAlchemy ORM Models
Move:
schemas/player_schema.py→app/schemas/player.pyUpdate imports:
4.3 Migrate Pydantic Models
Move:
models/player_model.py→app/models/player.pyNo import changes needed (uses only Pydantic):
4.4 Migrate Service Layer
Move:
services/player_service.py→app/services/player.pyUpdate imports:
Phase 5: Update Configuration Files
5.1 Update
DockerfileChange the uvicorn command:
5.2 Update
compose.yamlUpdate the command:
5.3 Update
pyproject.toml(if using)Update any references to main module:
Phase 6: Update Tests
6.1 Update
tests/conftest.pyUpdate imports:
6.2 Update
tests/test_main.pyUpdate all imports:
6.3 Update
tests/player_stub.pyUpdate imports:
Phase 7: Cleanup and Documentation
7.1 Remove Old Directories
rm -rf routes/ rm -rf models/ rm -rf schemas/ rm -rf services/ rm -rf databases/ rm main.py # Now in app/main.py7.2 Update
README.md7.3 Update
.gitignore(if needed)Ensure
__pycache__inapp/is ignored:Phase 8: Optional Enhancements
8.1 Add
app/dependencies.py(Future Use)8.2 Update Router to Use Dependencies
Phase 9: Integration with Other Issues
9.1 Alembic Configuration
If implementing Alembic migrations (see related issue), update
alembic/env.py:9.2 SQLModel Integration
If implementing SQLModel (see related issue), the structure remains the same:
The
schemas/folder could be removed entirely with SQLModel since it unifies both layers.Acceptance Criteria
Structure & Organization
app/package is created with proper__init__.pyfiles in all subpackagesapp/packageroutes/,models/,schemas/,services/,databases/) are removedplayer.pyinstead ofplayer_route.pyCore Application
app/main.pyexists and initializes FastAPI applicationrouterinstead ofapi_router)from app.routers import player, healthapp.include_router(player.router)Code Migration
app/routers/health.py✅app/routers/player.py✅app/databases/player.py✅app/schemas/player.py✅app/models/player.py✅app/services/player.py✅Imports & Dependencies
app.*prefixTesting
tests/conftest.pyimports fromapp.*tests/test_main.pyimports fromapp.*tests/player_stub.pyimports fromapp.*pytestruns green ✅Deployment & Infrastructure
Dockerfileupdated to runuvicorn app.main:appcompose.yamlupdated with new application path/healthDocumentation
README.mdupdated with new project structureapp.main:apppathIntegration with Related Issues
alembic/env.pyimports fromapp.*Quality Assurance
/docsruff check .or equivalentReferences
Migration Strategy
Risk Assessment
Low Risk:
Potential Issues:
Rollback Plan
feature/restructure-app-packagegit revertis straightforwardTesting Strategy
pytestto catch import errors earlyuvicorn app.main:appTimeline Estimate
Communication
Before starting: