⚙️ You can Generate Project Interactively Based on this template with the FastAPI Gen8 CLI Tool
This repository provides a clean and scalable template for building FastAPI applications. It is designed to help you start new projects quickly with best practices in mind.
-
📘 Organized project structure
-
🗒️ Predefined Environment Configuration with Pydantic-Settings
-
🛜 Dependency management Setup for Common Dependencies
get_db: Async Database Session Dependencyget_current_user: Async User dependency. Extracts the user from the request's access token, and raises a 401 if the token is missing, invalid, blacklisted, or is not an access token.
-
👤 Initial User Model and User Authentication Endpoints with Unit Tests
-
🔐 Full JWT auth flow: signup → email activation, sign-in issuing separate access and refresh tokens (each tagged with a
typeclaim so they are not interchangeable), password reset, profile update, and logout via a Redis token blacklist. -
🧰 Async Redis manager (
redis.asyncio) backing the token blacklist and short-lived one-time codes (activation / password reset). -
🔒 Docs (
/docs,/redoc,/openapi.json) gated behind aDEBUGflag or an IP allowlist — hidden with a 404 otherwise. -
🚦 Per-endpoint rate limiting on the auth routes via
slowapi, plus per-account lockout after repeated bad codes (brute-force protection on login and code endpoints). -
🩺
/healthreadiness probe and a startup connectivity check for the database and Redis. -
📝 Predefined Logging Configuration
-
⚙️ Unit Test Configuration with Pytest (With Async Support)
-
⏺️ Alembic Data Migration Configuration and alembic.ini
Requires Python 3.12+ (the codebase uses PEP 695 generic syntax).
In order to get started with the FastAPI Project, follow the following steps
-
Activate Project Python Virtual Environment
source venv/bin/activate # this is for Unix systems
-
Create an .env file from the .env.example file and provide values for missing environment variables - 1. Update the DATABASE_URL to point at an accessible DATABASE server - 2. Update the MAIL_CONFIG section to include mail server credentials - 3. Set
DEBUG=Truefor local development (also exposes the interactive docs — see Quirks) -
Ensure a Redis server is running and reachable at
REDIS_HOST/REDIS_PORT(defaults tolocalhost:6379). The auth flows and the test suite talk to a real Redis instance. -
Install
makeif you do not already have it and run the commandmake run-localto start you local server -
Apply Initial Database Migration for Ensure Database Connection string is valid
bash alembic upgrade head -
Ensure the Setup Is Complete and Sucessful by Running the following command
bash make test-localIf all the tests pass successfully you're good to start working on your project. -
Start Local Server with the following command
bash make run-local
The template is async-first and organizes each feature across four layers. When you add a feature, follow the same shape the auth feature uses:
| Layer | Responsibility |
|---|---|
routers/ |
HTTP endpoints, dependency wiring, and response_model. Kept thin — no business logic. |
services/ |
Business logic: DB queries, token/password/email orchestration, Redis access. |
schemas/ |
Pydantic request/response models — all validation lives here. |
models/ |
SQLAlchemy ORM models (persistence). All inherit AbstractBase → UUID PK + date_created/date_updated. |
Request flow: a router aggregates into app/api_router.py under the /v1 prefix, which is mounted in app/main.py. main.py also assembles the middleware stack (CORS → GZip → TrustedHost → docs gate → request logging), a slowapi rate limiter, and uniform JSON exception handlers.
Supporting singletons: redis_manager (async Redis for the token blacklist and one-time codes) and send_mail (Jinja templates from app/templates/, always dispatched via FastAPI BackgroundTasks).
app/schemas/__init__.py provides a reusable generic envelope for list endpoints, PaginatedResponse[T], so paginated payloads share one consistent shape:
| Field | Meaning |
|---|---|
total_results |
total rows matching the query |
current_page |
1-based page number returned |
total_pages |
total number of pages |
per_page |
page size used |
results |
the page of items, typed as list[T] |
Parameterize it with the item schema and use it as the endpoint's response_model:
from app.schemas import PaginatedResponse
from app.schemas.auth import UserModel
@router.get("/users", response_model=PaginatedResponse[UserModel])
async def list_users(db: DBDep, page: int = 1, per_page: int = 20):
# ... run the query, collect `users` and `total_results` ...
return PaginatedResponse[UserModel](
total_results=total_results,
current_page=page,
total_pages=-(-total_results // per_page), # ceiling division
per_page=per_page,
results=users,
)The model uses PEP 695 generic syntax (
class PaginatedResponse[T]), which needs Python 3.12+ and mypy ≥ 1.12 — the CI workflows and the pre-commitmypypin are set accordingly. On an older toolchain you'd seeName "T" is not defined; bump the versions (or fall back to the classicGeneric[T]+TypeVarform).
fastapi-project-structure/
.
├── Makefile
├── app
│ ├── __init__.py
│ ├── api_router.py
│ ├── database.py
│ ├── dependencies.py
│ ├── logger.py
│ ├── main.py
│ ├── middlewares.py
│ ├── models
│ │ ├── __init__.py
│ │ └── auth.py
│ ├── routers
│ │ ├── __init__.py
│ │ └── auth.py
│ ├── schemas
│ │ ├── __init__.py
│ │ └── auth.py
│ ├── services
│ │ ├── __init__.py
│ │ └── auth.py
│ └── settings.py
├── logs
└── requirements.txt
Things that are easy to trip over when building on this template:
- Alembic only sees models imported in
app/models/__init__.py. After adding a model, import it there (and add it to__all__) before runningalembic revision --autogenerate— otherwise the migration silently misses your table. - Redis is required and its client is async. Auth flows (logout blacklist, activation/reset codes) and the test suite hit a real Redis server. Every
redis_managercall is a coroutine —awaitit. The test suite closes the connection pool after each test (autouse fixture inconftest.py) becausepytest-asynciogives each test its own event loop; a sharedredis.asynciopool would otherwise reuse a closed-loop socket and raiseEvent loop is closed. - Docs are gated by
DEBUGOR the IP allowlist. TheAllowAuthorizedDocAccessmiddleware serves/docs,/redoc, and/openapi.jsononly whensettings.DEBUGis true or the client IP is inallowed_ips(default127.0.0.1); otherwise it returns a 404 that hides their existence. Note this middleware runs beforeTrustedHostMiddleware, so a request that clears the docs gate must still use a host listed inmain.py'sallowed_hosts. - Access and refresh tokens are not interchangeable. Each carries a
typeclaim (access/refresh).get_current_userrejects anything that isn't an access token; therefresh_tokenendpoint rejects anything that isn't a refresh token. - Refresh tokens are single-use (rotated). Each call to
/refresh_tokenblacklists the presented refresh token and returns a fresh access and refresh token, so a leaked refresh token is usable at most once. - Logout is global, and so is a password change. Logout blacklists the presented token(s) and bumps a per-user token version in Redis, so every token issued before it is invalidated across all devices (tokens carry a
verclaim checked on each request). A successful password reset or change bumps the same version — revoking all existing sessions, including the current one. Reusing an already-rotated refresh token is treated as theft and revokes the whole family. - Baseline security headers are added to every response by
SecurityHeadersMiddleware(X-Content-Type-Options,X-Frame-Options,Referrer-Policy, and HSTS outsideDEBUG). No CSP is set, to avoid breaking Swagger UI. - Behind a proxy, run with forwarded headers (
make run-prod/uvicorn --proxy-headers --forwarded-allow-ips=...) — otherwise per-IP rate limiting and logging see the load balancer's IP, not the client's. Set--forwarded-allow-ipsto your proxy's IP, never"*"(spoofable). This also keeps the docs IP-allowlist meaningful — without it a co-located proxy makes every client look like127.0.0.1. - Auth is built to resist enumeration. Login runs bcrypt even for unknown emails (constant-ish timing), and
/signupreturns the same generic message whether or not the email is registered (so it no longer returns the created user). Reset/activation emails are additionally throttled per-account (cooldown) on top of the per-IP rate limit. JWT_SECRETmust be ≥ 32 chars in production, requests overMAX_REQUEST_BODY_BYTES(1 MB) get a 413, and every route has a120/minutedefault rate-limit backstop beneath the stricter per-route limits.- Repeated bad codes lock the account. After
MAX_CODE_ATTEMPTS(default 5) wrong activation/reset codes, that account is locked forCODE_LOCKOUT_SECONDS; a successful attempt clears the counter. /healthand startup checks./healthreturns 503 if the DB or Redis is unreachable. On boot the app pings both; in production (DEBUG=False) it refuses to start if either is down, inDEBUGit only logs.- Sign-in requires a verified email.
signin_userreturns403 Email not verifieduntil activation flipsis_verified. In tests,UserFactorybuilds verified users; usecreate_user/an unverified user to exercise the rejection. - Auth endpoints are rate-limited via
slowapi(@limiter.limitinapp/routers/auth.py, registered inapp/main.py). Limits are Redis-backed (app/limiter.py) so they hold across workers/replicas. The limiter is disabled in the test suite (conftest.py) since the counter is shared across tests — enable it per-test (with a unique client key) to assert 429s. - Access tokens are short-lived; sign secrets are enforced in production. Access tokens default to 15 minutes (
ACCESS_TOKEN_LIFESPAN_MIN), refresh tokens to 28 days (REFRESH_TOKEN_LIFESPAN_DAYS). WithDEBUG=False, an emptyJWT_SECRETmakes the app refuse to start. - One-time codes are single-use and cryptographically random. Activation and password-reset codes come from
secretsand are deleted from Redis on successful use, so they can't be replayed. app/main.pycontains{{ project_name }}-style placeholders (title/version/summary). These are template placeholders meant to be filled in per project, not bugs.- Mail sends with
VALIDATE_CERTS=True. If your dev SMTP uses a self-signed certificate, adjust theConnectionConfiginapp/mailer.py.
-
Clone the repository:
git clone https://github.com/brianobot/fastAPI_project_structure.git cd fastAPI_project_structure -
Create & Activate Virtual Environment to Manage Project Dependency In Isolation
python3 -m venv venv && source venv/bin/activate #for unix computers
-
Install dependencies:
pip install -r requirements.txt
-
Run the application:
make run-local # or uvicorn app.main:app --reload -
Access the API docs:
- Open http://localhost:8000/docs in your browser.
Run initial tests using pytest:
make test-local # or pytestor
Run Specific tests
pytest -s app/routers/tests/test_auth.pyCopy .env.example to .env and update the values as needed. Notable keys:
DATABASE_URL(required) — async driver expected, e.g.postgresql+asyncpg://...DEBUG(defaultFalse) — whenTrue, exposes the interactive docs to all clients (see Quirks)REDIS_HOST/REDIS_PORT(defaultlocalhost/6379)JWT_SECRET— signing key; required whenDEBUG=False(the app refuses to boot with an empty secret in production).JWT_ALGORITHMdefaults toHS256.ACCESS_TOKEN_LIFESPAN_MIN(default15, minutes) /REFRESH_TOKEN_LIFESPAN_DAYS(default28, days)MAIL_*— SMTP credentials used by the mailer
Backporting these changes into a project scaffolded from an older version of the template? Follow UPGRADING.md — it isolates each change so you can apply them independently, and keeps rate limiting an optional, skippable step.
Contributions are welcome! Please open issues or submit pull requests.