A FastAPI backend with Docker development environment, Poetry dependency management, and Ruff for linting/formatting.
# Copy environment file
cp .env.example .env
# Install dependencies and compile email templates
make install
# Start development server (includes migrations)
make up- API: http://localhost:8000
- Docs: http://localhost:8000/docs
- Admin: http://localhost:8000/admin
- FastAPI - Modern async web framework
- SQLModel - SQL database ORM
- Alembic - Database migrations
- PostgreSQL - Production database
- SQLAdmin - Admin panel for SQLAlchemy/SQLModel
- Firebase Admin - Authentication via Firebase ID tokens
- Resend - Email delivery for password resets
- Poetry - Dependency management
- Ruff - Linting & formatting
- Docker - Containerization
- Docker & Docker Compose
-
Clone and navigate to the project:
cd fastback -
(Optional) Create environment file:
cp .env.example .env
-
Start the development server:
make up
This automatically runs database migrations after starting the containers.
-
Access the API:
- API: http://localhost:8000
- Docs (Swagger): http://localhost:8000/docs
- Docs (ReDoc): http://localhost:8000/redoc
- Admin (SQLAdmin): http://localhost:8000/admin
| Variable | Required | Description |
|---|---|---|
DB_USER |
Yes | PostgreSQL username |
DB_PASSWORD |
Yes | PostgreSQL password |
DB_HOST |
Yes | PostgreSQL host |
DB_PORT |
Yes | PostgreSQL port |
DB_NAME |
Yes | PostgreSQL database name |
DATABASE_URL |
Yes | Full connection string (constructed from values above) |
GOOGLE_APPLICATION_CREDENTIALS |
Yes | Path to Firebase service account JSON |
FIREBASE_API_KEY |
Yes | Firebase API key for Identity Toolkit |
SESSION_SECRET_KEY |
Yes | Secret key for admin sessions |
ADMIN_USERNAME |
Yes | SQLAdmin username |
ADMIN_PASSWORD |
Yes | SQLAdmin password |
RESEND_API_KEY |
No | Resend API key for password reset emails |
ENV_NAME |
No | Environment name (default: development) |
APP_DOMAIN |
No | Domain for emails (default: resend.dev) |
CLIENT_URL |
No | Client app URL (default: http://localhost:3000) |
CORS_ORIGINS |
No | Comma-separated allowed origins (default: *) |
SESSION_EXPIRES_DAYS |
No | Session cookie expiration in days (default: 5, range: 1–14) |
LOG_LEVEL |
No | Log level (default: INFO) |
LOG_JSON |
No | JSON logs (true/false, default: false) |
LOG_REQUESTS |
No | Log each HTTP request (default: true) |
LOG_UVICORN_ACCESS |
No | Uvicorn access logs (default: false; enabled if set true) |
HTTPX_LOG_LEVEL |
No | httpx log level (default: WARNING) |
FastBack configures logging to stdout and includes an HTTP request logging middleware.
- Request logs: enabled by default via
LOG_REQUESTS=true - Avoid duplicate access logs: when
LOG_REQUESTS=true, Uvicorn access logs default to off (setLOG_UVICORN_ACCESS=trueto enable) - Structured logging: set
LOG_JSON=truefor JSON output
Example (JSON logs):
LOG_JSON=true LOG_LEVEL=INFO make upFirebase Admin SDK is used for token verification. Protected routes require a valid Firebase ID token in the Authorization header.
- Download your Firebase service account JSON from the Firebase Console
- Set
GOOGLE_APPLICATION_CREDENTIALSto the path of the JSON file
from app.auth.dependencies import CurrentUserDep
@router.get("/protected")
async def protected_route(user: CurrentUserDep):
return {"user_id": user.id, "email": user.email}curl -H "Authorization: Bearer YOUR_FIREBASE_ID_TOKEN" http://localhost:8000/users/meThis project uses Alembic for database schema migrations with autogenerate support.
After modifying models in app/models/, create a new migration:
make migrate-new MSG="add email field to user"This generates a migration file in alembic/versions/ with the detected changes.
make migratemake migrate-downmake migrate-historySQLAdmin is mounted at /admin.
Run make help to see all available commands:
| Command | Description |
|---|---|
make install |
Install dependencies, compile emails, setup hooks |
make compile-emails |
Compile email templates (inline CSS, minify) |
make up |
Start Docker dev server (runs migrations) |
make up-d |
Start in detached mode (runs migrations) |
make down |
Stop containers |
make down-v |
Stop containers and remove volumes |
make down-all |
Stop containers, remove volumes, images, and orphans |
make logs |
Tail container logs |
make sh |
Shell into container |
make format |
Format code with Ruff |
make lint |
Lint code with Ruff |
make fix |
Auto-fix lint issues |
make test |
Run tests |
make test-cov |
Run tests with coverage report |
make test-cov-html |
Run tests with HTML coverage report |
make migrate |
Run database migrations |
make migrate-new MSG="message" |
Create new migration |
make migrate-down |
Rollback last migration |
make migrate-history |
Show migration history |
Hot reload is enabled—code changes reflect automatically without rebuilding.
# Shell into the container
make sh
# Add a package
poetry add <package-name>
# Add a dev dependency
poetry add --group dev <package-name>fastback/
├── app/
│ ├── main.py # FastAPI application entry
│ ├── router.py # Central router aggregation
│ ├── admin/ # SQLAdmin UI and authentication
│ ├── alembic/ # Database migrations
│ ├── core/ # Shared utilities (settings, deps, exceptions)
│ ├── db/ # Database engine and sessions
│ ├── models/ # Shared models
│ ├── templates/ # Email and other templates
│ └── <domain>/ # Domain modules (auth, user, health, etc.)
│ ├── router.py # API endpoints
│ ├── service.py # Business logic
│ ├── models.py # Database models
│ ├── schemas.py # Request/response Pydantic models
│ ├── dependencies.py # Route dependencies
│ └── exceptions.py # Domain-specific exceptions
├── tests/ # Pytest test suite
├── docs/ # Documentation
└── scripts/ # Utility scripts
Each domain module follows this convention. Not all files are required—include only what the domain needs.
FastBack uses a unified exception system with automatic HTTP status code mapping and consistent error response formatting.
All custom exceptions inherit from AppException and define their own status_code and error_type. Exceptions are automatically converted to JSON responses with the format:
{
"type": "error_type",
"message": "Error message"
}Simply raise the appropriate exception in your route handlers or services:
from app.core.exceptions import UserNotFoundError, EmailExistsError
@router.get("/user/{user_id}")
async def get_user(user_id: int, session: SessionDep):
user = session.get(User, user_id)
if not user:
raise UserNotFoundError(f"User with ID {user_id} not found")
return user| Exception | Error Type | Default Message |
|---|---|---|
AuthenticationError |
authentication_error |
"Authentication failed" |
InvalidCredentialsError |
invalid_credentials |
"Invalid email or password" |
InvalidTokenError |
invalid_token |
"Invalid authentication token" |
SessionCookieError |
session_cookie_error |
"Session cookie error" |
SessionExpiredError |
session_expired |
"Session has expired" |
| Exception | Error Type | Default Message |
|---|---|---|
AuthorizationError |
authorization_error |
"Access denied" |
UserDisabledError |
user_disabled |
"User account is disabled" |
UserInactiveError |
user_inactive |
"User is inactive" |
| Exception | Error Type | Default Message |
|---|---|---|
NotFoundError |
not_found |
"Resource not found" |
UserNotFoundError |
user_not_found |
"User not found" |
| Exception | Error Type | Default Message |
|---|---|---|
ConflictError |
conflict |
"Resource conflict" |
EmailExistsError |
email_exists |
"Email already registered" |
| Exception | Error Type | Default Message |
|---|---|---|
ValidationError |
validation_error |
"Validation failed" |
WeakPasswordError |
weak_password |
"Password is too weak" |
PasswordPolicyError |
password_policy_error |
"Password does not meet requirements" |
BadRequestError |
bad_request |
"Bad request" |
EmailVerificationError |
email_verification_error |
"Email verification failed" |
| Exception | Error Type | Default Message |
|---|---|---|
RateLimitError |
rate_limit_exceeded |
"Too many requests, please try again later" |
| Exception | Error Type | Default Message |
|---|---|---|
ExternalServiceError |
external_service_error |
"External service error" |
ProviderError |
provider_error |
"Authentication provider returned an invalid response" |
| Exception | Error Type | Default Message |
|---|---|---|
InternalError |
internal_error |
"An internal error occurred" |
All exceptions accept a custom message:
from app.core.exceptions import UserNotFoundError
raise UserNotFoundError("Custom error message")PasswordPolicyError accepts an optional requirements parameter:
from app.core.exceptions import PasswordPolicyError
raise PasswordPolicyError(
message="Password validation failed",
requirements=["Must be at least 8 characters", "Must contain uppercase"]
)This project follows SOLID principles to maintain clean, maintainable code:
Each module has one clear responsibility:
| Module | Responsibility |
|---|---|
app/auth/router.py |
Authentication endpoints |
app/auth/service.py |
Firebase service abstraction |
app/auth/dependencies.py |
Auth dependencies (CurrentUserDep) |
app/user/router.py |
User management endpoints |
app/user/models.py |
User database model |
app/router.py |
Router aggregation only |
app/db/engine.py |
Database engine and session management |
app/core/settings.py |
Typed application configuration |
app/core/deps.py |
Shared dependency providers |
app/core/constants.py |
Route prefixes and tags |
app/core/firebase.py |
Firebase SDK initialization |
app/core/email.py |
Email delivery via Resend |
app/core/cors.py |
CORS middleware configuration |
- Extensible routing: Add new domain modules (e.g.,
app/posts/) with their own router and include it inapp/router.py—no modification to existing route modules required - Model extension: New models can be added without modifying existing ones
- SQLModel models extend
SQLModelbase class correctly - Session dependencies can be mocked/substituted in tests
- Route modules expose only a minimal
routerobject app/dbexports only what consumers need:engine,get_sessionapp/core/depsprovides focused dependency aliases:SessionDep,SettingsDepapp/auth/dependenciesprovides auth-specific dependencies:CurrentUserDep,AdminUserDep,FirebaseAuthDep
- Routes depend on abstractions (
SessionDep,CurrentUserDep,SettingsDep), not concrete implementations - Configuration is loaded via Pydantic Settings with environment variable injection
from app.core.deps import SessionDep
@router.get("/items")
async def get_items(session: SessionDep):
# session is automatically injected
return session.exec(select(Item)).all()from app.auth.dependencies import CurrentUserDep
@router.get("/me")
async def get_me(user: CurrentUserDep):
# user is the authenticated User model from the database
return {"id": user.id, "email": user.email}