-
Notifications
You must be signed in to change notification settings - Fork 18
Description
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.pyrepeat 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
routerinstead ofapi_router - Creating clear package boundaries for better imports and testing
Benefits:
- β
Cleaner imports:
from app.routers import playervsfrom 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.pyinstead ofplayer_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__.py1.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 namePhase 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 unchanged4.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 unchanged4.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 unchanged4.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 implementationPhase 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 --reload5.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 app6.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 == 2006.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."""
# ... implementationPhase 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.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 90007.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 import9.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
-
app/package is created with proper__init__.pyfiles in all subpackages - All application logic resides under
app/package - Old top-level folders (
routes/,models/,schemas/,services/,databases/) are removed - File naming follows conventions:
player.pyinstead ofplayer_route.py
Core Application
-
app/main.pyexists and initializes FastAPI application - All routers use standardized naming (
routerinstead ofapi_router) - Router imports use new paths:
from app.routers import player, health - Router registration updated:
app.include_router(player.router)
Code Migration
- Health router migrated:
app/routers/health.pyβ - Player router migrated:
app/routers/player.pyβ - Database config migrated:
app/databases/player.pyβ - SQLAlchemy models migrated:
app/schemas/player.pyβ - Pydantic models migrated:
app/models/player.pyβ - Service layer migrated:
app/services/player.pyβ
Imports & Dependencies
- All imports throughout the codebase use
app.*prefix - Cross-layer imports work correctly (routers β services β schemas)
- No circular import issues exist
- IDE autocomplete and navigation work properly
Testing
- All test files updated with new import paths
-
tests/conftest.pyimports fromapp.* -
tests/test_main.pyimports fromapp.* -
tests/player_stub.pyimports fromapp.* - All existing tests pass:
pytestruns green β - Test coverage remains at or above current level
Deployment & Infrastructure
-
Dockerfileupdated to runuvicorn app.main:app -
compose.yamlupdated with new application path - Container builds successfully
- Application runs in Docker container
- Health check endpoint accessible at
/health
Documentation
-
README.mdupdated with new project structure - Architecture section added explaining layer organization
- Running instructions updated with
app.main:apppath - Any inline documentation updated to reflect new structure
Integration with Related Issues
- If Alembic is implemented:
alembic/env.pyimports fromapp.* - If SQLModel is implemented: Structure accommodates unified models
- No conflicts with ongoing feature branches
Quality Assurance
- No broken imports remain in codebase
- Application starts without errors
- All API endpoints respond correctly
- OpenAPI documentation generates properly at
/docs - No regression in functionality
- Code passes linting:
ruff check .or equivalent - Type checking passes (if using mypy/pyright)
References
- [FastAPI Bigger Applications Tutorial](https://fastapi.tiangolo.com/tutorial/bigger-applications/)
- [Python Package Structure Best Practices](https://docs.python-guide.org/writing/structure/)
- [FastAPI Project Structure Guide](https://fastapi.tiangolo.com/tutorial/bigger-applications/#an-example-file-structure)
- Related: [FEATURE] Implement Alembic for Database MigrationsΒ #2 Database Migration Tool Implementation (Alembic)
- Related: [FEATURE] Consolidate Player Models with SQLModelΒ #173 Consolidate Player Models with SQLModel
- [Real World FastAPI Example](https://github.com/nsidnev/fastapi-realworld-example-app)
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 revertis straightforward
Testing Strategy
- After each migration phase: Run
pytestto 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:
- Notify team of upcoming structural changes
- Coordinate with anyone working on feature branches
- Schedule migration during low-activity period
- Prepare team for import path changes in their branches