Skip to content

[FEATURE] Restructure to FastAPI Bigger Applications PatternΒ #385

@nanotaboada

Description

@nanotaboada

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:

  1. Scalability Issues: As features grow, flat structure becomes difficult to navigate
  2. Import Complexity: Long import paths like from routes.player_route import api_router
  3. Naming Redundancy: Files named player_route.py, player_model.py, player_schema.py repeat context
  4. Non-idiomatic: Doesn't follow [FastAPI's recommended "bigger applications" pattern](https://fastapi.tiangolo.com/tutorial/bigger-applications/)
  5. Testing Confusion: Test imports are verbose and unclear about package boundaries
  6. Deployment Ambiguity: No clear application entry point package
  7. 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:

  1. Grouping all application logic under a single app/ package
  2. Organizing by layer (routers, models, schemas, services, databases) within app/
  3. Removing redundant suffixes from filenames (folder names provide context)
  4. Standardizing router naming to just router instead of api_router
  5. 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

  • app/ package is created with proper __init__.py files 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.py instead of player_route.py

Core Application

  • app/main.py exists and initializes FastAPI application
  • All routers use standardized naming (router instead of api_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.py imports from app.*
  • tests/test_main.py imports from app.*
  • tests/player_stub.py imports from app.*
  • All existing tests pass: pytest runs green βœ…
  • Test coverage remains at or above current level

Deployment & Infrastructure

  • Dockerfile updated to run uvicorn app.main:app
  • compose.yaml updated with new application path
  • Container builds successfully
  • Application runs in Docker container
  • Health check endpoint accessible at /health

Documentation

  • README.md updated with new project structure
  • Architecture section added explaining layer organization
  • Running instructions updated with app.main:app path
  • Any inline documentation updated to reflect new structure

Integration with Related Issues

  • If Alembic is implemented: alembic/env.py imports from app.*
  • 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


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

  1. Keep original structure in Git history
  2. Create feature branch: feature/restructure-app-package
  3. Test thoroughly before merging to main
  4. If issues arise: git revert is straightforward

Testing Strategy

  1. After each migration phase: Run pytest to catch import errors early
  2. Before Docker update: Test locally with uvicorn app.main:app
  3. After Docker update: Test container build and runtime
  4. 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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestpythonPull requests that update Python code

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions