# FastAPI Project Structure (uv)

> Standard project layout for FastAPI with uv package manager. Router / Service / Repository separation.

---

## Project Structure

```
project-name/
├── pyproject.toml            # uv project config + dependencies
├── .python-version           # Python version pin
├── .env                      # Environment variables
├── .env.example              # Template for .env
├── main.py                   # Entry point (uvicorn)
├── app/
│   ├── __init__.py
│   ├── config.py             # Settings (pydantic-settings)
│   ├── dependencies.py       # Shared FastAPI dependencies
│   ├── api/
│   │   ├── __init__.py
│   │   ├── router.py         # Root router (include all module routers)
│   │   └── v1/               # API versioning
│   │       ├── __init__.py
│   │       └── <module>.py   # Module router
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── common.py         # Shared response/pagination schemas
│   │   └── <module>.py       # Module request/response schemas
│   ├── services/
│   │   ├── __init__.py
│   │   └── <module>.py       # Business logic
│   ├── repositories/
│   │   ├── __init__.py
│   │   └── <module>.py       # Data access (SQL/ORM)
│   ├── models/
│   │   ├── __init__.py
│   │   └── <module>.py       # Domain models / DB models
│   ├── core/
│   │   ├── __init__.py
│   │   ├── database.py       # DB connection setup
│   │   ├── exceptions.py     # Custom exceptions + handlers
│   │   └── http_client.py    # External API client (httpx)
│   └── utils/
│       ├── __init__.py
│       └── <helpers>.py      # Pure utility functions
└── tests/
    ├── __init__.py
    ├── conftest.py            # Fixtures
    └── test_<module>.py
```

---

## Data Flow

```
HTTP Request
  -> Router (validate input via schema, delegate)
    -> Service (business logic, orchestrate)
      -> Repository (data access, SQL)
      -> External client (httpx, third-party APIs)
    -> Schema (shape response)
  -> HTTP Response

Background Task
  -> Service (logic)
    -> Repository / External client
```

---

## Layer Responsibilities

| Layer | File | Does | Does NOT |
|-------|------|------|----------|
| **Router** | `api/v1/*.py` | Validate input (schema), call service, return response | Business logic, DB access |
| **Schema** | `schemas/*.py` | Define request/response shape, validation (pydantic) | Logic, DB access |
| **Service** | `services/*.py` | Business logic, orchestrate repos + external calls | Direct HTTP handling, SQL |
| **Repository** | `repositories/*.py` | Data access (SQL queries, ORM operations) | Business logic, validation |
| **Model** | `models/*.py` | Domain/DB model definition | HTTP handling, business logic |
| **Core** | `core/*.py` | DB setup, exception handling, external HTTP client | Business logic |

---

## uv Setup

### Init project

```bash
uv init project-name
cd project-name
```

### Add dependencies

```bash
uv add fastapi uvicorn[standard] pydantic-settings httpx
uv add sqlalchemy asyncpg          # if using PostgreSQL
uv add --dev pytest pytest-asyncio httpx
```

### Run

```bash
uv run uvicorn main:app --reload --port 8000
```

### pyproject.toml

```toml
[project]
name = "project-name"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.115",
    "uvicorn[standard]>=0.34",
    "pydantic-settings>=2.7",
    "httpx>=0.28",
]

[project.scripts]
dev = "uvicorn main:app --reload --port 8000"

[dependency-groups]
dev = [
    "pytest>=8.0",
    "pytest-asyncio>=0.24",
    "httpx>=0.28",
]
```

---

## Core Components

### 1. Entry Point

```python
# main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.api.router import api_router
from app.core.exceptions import register_exception_handlers

app = FastAPI(
    title=settings.APP_NAME,
    version="0.1.0",
    docs_url="/docs" if settings.DEBUG else None,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

register_exception_handlers(app)
app.include_router(api_router, prefix="/api")
```

### 2. Config

```python
# app/config.py

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    APP_NAME: str = "My API"
    DEBUG: bool = True
    CORS_ORIGINS: list[str] = ["http://localhost:3000"]

    DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost:5432/db"

    # External APIs
    EXTERNAL_API_URL: str = "https://api.example.com"
    EXTERNAL_API_TIMEOUT: int = 30

    model_config = SettingsConfigDict(env_file=".env")

settings = Settings()
```

### 3. Root Router

```python
# app/api/router.py

from fastapi import APIRouter
from app.api.v1 import module

api_router = APIRouter()
api_router.include_router(module.router, prefix="/v1")
```

### 4. Database Setup

```python
# app/core/database.py

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.config import settings

engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)

async def get_session() -> AsyncSession:
    async with SessionLocal() as session:
        yield session
```

### 5. Exception Handling

```python
# app/core/exceptions.py

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

class AppError(Exception):
    def __init__(self, message: str, status_code: int = 400, code: str | None = None):
        self.message = message
        self.status_code = status_code
        self.code = code

class NotFoundError(AppError):
    def __init__(self, message: str = "Not found"):
        super().__init__(message, status_code=404, code="NOT_FOUND")

class ValidationError(AppError):
    def __init__(self, message: str = "Validation error"):
        super().__init__(message, status_code=422, code="VALIDATION_ERROR")

def register_exception_handlers(app: FastAPI):
    @app.exception_handler(AppError)
    async def app_error_handler(request: Request, exc: AppError):
        return JSONResponse(
            status_code=exc.status_code,
            content={"detail": exc.message, "code": exc.code},
        )
```

### 6. External HTTP Client

```python
# app/core/http_client.py

import httpx
from app.config import settings

class ExternalClient:
    def __init__(self, base_url: str, timeout: int = 30):
        self.client = httpx.AsyncClient(
            base_url=base_url,
            timeout=timeout,
        )

    async def get(self, path: str, **kwargs) -> dict:
        response = await self.client.get(path, **kwargs)
        response.raise_for_status()
        return response.json()

    async def post(self, path: str, data: dict, **kwargs) -> dict:
        response = await self.client.post(path, json=data, **kwargs)
        response.raise_for_status()
        return response.json()

    async def close(self):
        await self.client.aclose()

# Singleton for app lifetime
external_client = ExternalClient(
    base_url=settings.EXTERNAL_API_URL,
    timeout=settings.EXTERNAL_API_TIMEOUT,
)
```

### 7. Common Schemas

```python
# app/schemas/common.py

from pydantic import BaseModel

class MessageResponse(BaseModel):
    message: str

class ErrorResponse(BaseModel):
    detail: str
    code: str | None = None
```

### 8. Dependency Injection

```python
# app/dependencies.py

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_session
from app.repositories.module import ModuleRepository
from app.services.module import ModuleService

def get_module_service(
    session: AsyncSession = Depends(get_session),
) -> ModuleService:
    repo = ModuleRepository(session)
    return ModuleService(repo)
```

---

## Naming Conventions

| Item | Convention | Example |
|------|-----------|---------|
| Project folder | `kebab-case` | `ghost-runner-api` |
| Python files | `snake_case` | `module_service.py` |
| Class | `PascalCase` | `ModuleService` |
| Function | `snake_case` | `get_list` |
| Constant | `UPPER_SNAKE_CASE` | `DATABASE_URL` |
| Schema (request) | `PascalCase + Request` | `CreateModuleRequest` |
| Schema (response) | `PascalCase + Response` | `ModuleDetailResponse` |
| Router tag | `lowercase` | `"modules"` |
| API prefix | `/api/v1/<module>` | `/api/v1/gpx` |

---

## Anti-Patterns

| Anti-Pattern | Correct Approach |
|-------------|-----------------|
| Business logic in router | Router is thin, move logic to service |
| SQL in service | Use repository for all data access |
| Direct `httpx.get()` in service | Use `core/http_client.py` wrapper |
| Hardcoded config values | Use `app/config.py` with pydantic-settings |
| Return dict from repo | Return model instance, schema handles serialization |
| Sync DB operations | Use async SQLAlchemy + asyncpg |
| `pip install` / `requirements.txt` | Use `uv add` / `pyproject.toml` |
