0. Prerequisites
- Basic git commands
- Some knowledge of asymmetric cryptography and authentication
- Understanding of databases and Internet systems
- Some basic software design principles
---
1. Dependencies
sqlalchemy[asyncio] pulls in greenlet for the async bridge; asyncpg is the async PostgreSQL driver; pyjwt[crypto] adds RSA/ECDSA support for OIDC providers.
2. Project layout
Reference for multi-module structure: https://fastapi.tiangolo.com/tutorial/bigger-applications/
3. Configuration
Keep secrets in a git-ignored .env; commit .env.example as the template. Write DATABASE_URL directly with the async driver:
Define settings as a typed pydantic_settings.BaseSettings class, point model_config at the env file, and wrap construction in @lru_cache so it parses once. Typed fields are validated at startup. Access settings via attributes:
Docs:
https://docs.pydantic.dev/latest/concepts/pydantic_settings/
https://fastapi.tiangolo.com/advanced/settings/
4. Declarative base & model registration
Use a three-file split:
db/base_class.py— declaresclass Base(DeclarativeBase)and aMetaData(naming_convention=...)for deterministic constraint/index names.models/__init__.py— imports and re-exports every model. Add one line per new model.db/base.py— the Alembic import target; importsBaseandapp.modelsso all tables register onBase.metadata:
Models import Base from base_class. Confirm metadata is populated before the first migration:
5. Async engine & session
Create one AsyncEngine per process via create_async_engine(settings.async_database_url). Set pool_pre_ping=True and set expire_on_commit=False on the sessionmaker so ORM attributes stay readable after commit.
Expose the session as a FastAPI dependency:
Docs:
https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
https://fastapi.tiangolo.com/tutorial/dependencies/
6. DTO schemas
Keep ORM models and Pydantic schemas separate. Define a base schema with ConfigDict(from_attributes=True) and serialize ORM instances with Schema.model_validate(obj). Return schemas from routes. Use distinct …Create (input) and …Read (output) schemas.
7. Alembic configuration
Bootstrap with the async template:
In env.py:
- Import the aggregate base so metadata is populated:
from app.db.base import Base→target_metadata = Base.metadata. Inject the URL from settings, keeping
alembic.ini'ssqlalchemy.urlblank. Escape%as%%in the URL:CodeBlock Loading...
- Build the async engine with
poolclass=pool.NullPooland bridge withconnection.run_sync(do_run_migrations). Set
compare_type=Trueandcompare_server_default=Trueincontext.configure(...).Docs:
https://alembic.sqlalchemy.org/en/latest/cookbook.html#using-asyncio-with-alembic
https://alembic.sqlalchemy.org/en/latest/tutorial.html
https://alembic.sqlalchemy.org/en/latest/autogenerate.html
8. Migration workflow
Review every generated script before applying.
9. JWT authentication
Two modes — choose by who issues the token:
- Self-issued (
core/security.py): your own/loginexchanges credentials for an access token. Hash passwords withpasslib[bcrypt], sign with an HS256 secret, setiat/exp. See FastAPI — OAuth2 with Password (and hashing), Bearer with JWT. - OIDC / external IdP (
middleware/jwt_decoder.py): verify the provider's token against its JWKS. Cache thePyJWKClientwith@lru_cache.
In both, the verification call specifies an explicit algorithm allowlist and verifies audience and issuer:
Return a generic 401 on failure. Keep tokens and claims out of logs.
Docs: https://pyjwt.readthedocs.io/en/stable/
10. Auth dependency & route protection
Centralize verification in one dependency (api/deps.py) that reads the bearer token, validates it, and resolves the local User (creating one on first login by mapping the token's sub). Use a single token source — header bearer for APIs, or server-side session for browser flows. Protect any route with one line:
Docs: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/
11. Application assembly
In main.py, construct FastAPI(lifespan=...), dispose the engine on shutdown inside the lifespan handler, and mount routers with include_router(prefix="/api"). Add SessionMiddleware when using session-based auth. Run locally and use the auto-generated docs to exercise endpoints:
Docs:
https://fastapi.tiangolo.com/advanced/events/
https://fastapi.tiangolo.com/tutorial/bigger-applications/
12. Reference
- FastAPI: docs home · dependencies · bigger applications · settings · OAuth2 + JWT · lifespan events
- Alembic: docs home · tutorial · autogenerate · async cookbook
- SQLAlchemy: asyncio extension
- Supporting: pydantic-settings · PyJWT · asyncpg