FastAPI in Production
Production FastAPI
This article summarizes a tutorial on best practices for setting up a production-sound FastAPI service. Find the tutorial here. This is an overview on the product structure.
├── app
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── api_v1
│ │ │ ├── __init__.py
│ │ │ ├── api.py
│ │ │ └── endpoints
│ │ │ ├── __init__.py
│ │ │ └── recipe.py
│ │ └── deps.py
│ ├── backend_pre_start.py
│ ├── core
│ │ ├── __init__.py
│ │ └── config.py
│ ├── crud
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── crud_recipe.py
│ │ └── crud_user.py
│ ├── db
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── base_class.py
│ │ ├── init_db.py
│ │ └── session.py
│ ├── initial_data.py
│ ├── main.py
│ ├── models
│ │ ├── __init__.py
│ │ ├── recipe.py
│ │ └── user.py
│ ├── schemas
│ │ ├── __init__.py
│ │ ├── recipe.py
│ │ └── user.py
│ └── templates
│ └── index.html
├── poetry.lock
├── prestart.sh
├── pyproject.toml
├── README.md
└── run.sh
A Key Data Flow Example
- Flow of Data: Requests come into API, Pydantic models validate the incoming data, and CRUD utilities interact with the database using the ORM.
- Pydantic Schemas:
- Differentiation from SQLAlchemy: Pydantic models (in the schemas directory) define the structure of data the API accepts and returns, while SQLAlchemy models represent database tables.
- orm_mode: This setting enables Pydantic to work directly with SQLAlchemy models, ensuring relationships and other ORM-specific data is included when needed.
- Separation with ElementInDB: Such specific schemas allow to have fields specifically for database interaction that might not be sent back to the client.
API Directory - Error Handling (Part 5)
- The HTTPException class from FastAPI allows to raise errors with appropriate HTTP status codes.
- Raising for status:
- a HTTPException is raised with a status code of 404 (Not Found) and a descriptive error message.
- Note that the exception is raised and not returned, as returning it would cause a validation error.
Schemas Directory - Pydantic (Part 4)
Pydantic
leverages type annotations to streamline data validation and settings management.- Define precise data structures, ensuring that data conforms to expected formats and types.
- Instead of using plain dictionaries, one can model data classes inherited from Pydantic’s BaseModel.
- Pydantic supports standard Python types as well as recursive model composition, enabling representation of complex objects.
Key benefits include eliminating the need to learn a specialized syntax (making it IDE-friendly), validating both request/response data and configuration settings, customizable data types, integration with Python dataclasses, and exceptional performance.
DB Directory - SQLAlchemy (Part 6)
- SQLAlchemy Core: Provides a powerful toolkit for interacting with relational databases using SQL-like constructs translated into Python.
- SQLAlchemy ORM: Maps Python objects to database tables, streamlining database interactions.
- Declarative Base: SQLAlchemy’s declarative_base function is used to create a base class for defining database models (tables).
- Defining the tables using ORM:
- Inheritance: Classes inherit from the Base classes created.
- Columns: Properties like id, label, and email become database columns with types like Integer and String.
- Relationships: ForeignKey and relationship establish the link between items and their submitting users. This creates a crucial one-to-many relationship.
- Connecting to the Database: Engine and Sessions
- Connection String:
- The SQLALCHEMY_DATABASE_URI defines where your database data is stored (e.g. in a SQLite a file named example.db).
- Connection strings get more complex for other database systems (e.g., PostgreSQL).
- Engine Creation: a create_engine function establishes the connection to the database, using the connection string and any necessary configuration (like check_same_thread for SQLite).
- SQLAlchemy Session:
- The session object is how one interact with the database when using the ORM.
- It manages the changes and communication between your Python objects and the database.
- Connection String:
Directory CRUD - CRUD Utilities (Part 6)
- Purpose: CRUD classes (in the crud directory) provide reusable functions for database interaction (Create, Read, Update, Delete).
- Generics: The CRUDBase class uses generics (TypeVar) to make it adaptable to different SQLAlchemy models and Pydantic schemas.
- Inheritance: Subclasses like CRUDRecipe inherit from CRUDBase, customizing it for specific tables (like the Recipe table).
- Simplicity: These subclasses mainly define the correct models and schemas, while the database logic is handled by the base class.
Diretory Alembic: Managing Change (Part 6)
- Purpose: Alembic is designed to track and manage changes to your database schema over the lifespan of your application.
- Crucial for a production environment where data needs to be preserved while the application evolves.
- Key Files:
- env.py: Houses configuration info like your database connection string and references to your SQLAlchemy models.
- versions: Contains migrations scripts. Each script defines changes to be applied (upgrade()) and how to reverse them (downgrade()).
- alembic.ini: Configuration file for Alembic itself.
- Migration Workflow
- alembic upgrade head: Applies outstanding migrations to bring your database schema up-to-date.
- alembic revision –autogenerate -m “Some description”: This command inspects your models and automatically generates a migration script reflecting detected changes. You should always review these auto-generated scripts for accuracy.
API Versioning: Why It Matters (Part 8)
- Benefits of API Versioning
- Flexibility for Change: Versioning allows you to make changes to your API without breaking existing clients who rely on older versions. This is crucial as your application evolves and your API’s structure might need to change.
- Clear Communication: API versions make it transparent for users which version they’re working with, reducing confusion and potential compatibility issues.
- New api Directory: Decoupling API logic into an api directory sets the stage for organized versioning as your project scales.
- core/config.py: Using a Pydantic Settings class for configuration is a smart move. It enforces type safety, validation, and makes fetching values from environment variables convenient.
- Routing with Prefixes:
- api_router: In app/api_v1/api.py, the prefix /recipes ensures all endpoint routes defined in recipe.py will be nested under /recipes.
- root_router in app/main.py: The prefix settings.API_V1_STR (e.g., /api/v1) applied here sets the base for all API routes.
- Intentional Exclusion: Your root route (/), designed to render the HTML template, remains unversioned. This makes sense, as core UI logic might not need the same frequent versioning as the API itself.
Understanding Asynchronous Code (Part 9)
- Concurrency vs. Parallelism: Concurrency is about managing multiple things at once, while parallelism focuses on executing tasks simultaneously (e.g., on multiple CPU cores).
- Asyncio to the Rescue: Python’s asyncio library revolutionized the way we write asynchronous code.
- It introduced coroutines (async def) and the await keyword, giving us powerful tools for handling IO-bound operations efficiently.
-
The Event Loop: The heart of asyncio. It schedules coroutines, handles network operations, and manages the “pausing” and “resuming” of coroutines when they hit await points.
- Practical Example: Performance Gains
- httpx: A perfect choice since it offers async capabilities unlike the more common requests library.
- Async Coroutines: The async def get_reddit_top_async function is the core of our asynchronous logic.
- Async Context Manager: The async with httpx.AsyncClient() block ensures proper handling of HTTP connections.
- The ‘await’ Keyword: Signals potential suspension points, allowing the event loop to switch to other tasks while we wait for network responses.
- asyncio.gather: The key to concurrent execution, allowing us to fetch data from multiple subreddits simultaneously.
- Add Middleware to measure response times
- While SQLAlchemy added async support, it requires careful consideration and different approaches.
Authentication and Authorization (Part 10)
- Authentication: Verifies a user’s identity.
- Authorization: Determines the permissions a user has.
- JWTs: Why and How
- JWT Structure: three parts of a JWT (header, payload, signature)
- JWTs provide a secure way to transmit authentication information
- Use Cases: You highlight scenarios where JWT auth is a great fit, especially in web apps with separate frontends and microservice architectures.
Practical Implementation
- Signup Endpoint
- Pydantic Models: Leverages Pydantic for request validation and response shaping.
- Dependency Injection: Relies on FastAPI’s DI system to access the database.
- Error Handling: Uses HTTPException to signal a conflict if a user with the same email already exists.
- CRUD Utilities: Employs CRUD functions to interact with the database.
- Password Handling
- Hashing with passlib: Ensures passwords are never stored in plain text.
- CryptContext: Provides convenient methods for hashing and verifying passwords.
- Login Endpoint
- OAuth2PasswordRequestForm: A built-in FastAPI class for handling login form data.
- Token Generation: The create_access_token function carefully crafts a JWT containing expiration data and the user’s ID as a subject.
- Authentication Middleware
- /me Endpoint: Demonstrates how to protect an endpoint requiring authentication.
- get_current_user Dependency: The core logic for extracting the user from a valid JWT, verifying its signature, and looking up the user in the database.
Areas for Further Exploration
- Authorization with Scopes: Fine-grained control over what actions authenticated users can perform.
- Refresh Tokens: Mechanism to extend token validity without requiring users to re-login frequently.
- Password Reset: Secure password recovery flows.
- SSO: Allowing users to authenticate with external providers.
- Advanced JWT Features: Custom payload data and encryption.
Dependency Injection (Part 11)
- Clarity: We implement DI and its reliance on composition to achieve inversion of control.
- FastAPI’s Elegance: No need for complex registration processes – it just works! This focus on developer-friendliness is one of the framework’s key merits.
- Practical Value: code that’s testable, reusable, and less repetitive.
Examples: Learning by Doing
You guide us through a series of excellent examples:
- Database Sessions: Revisiting the familiar database session dependency cements the core pattern: definining a callable and injecting it with Depends().
- Reddit Client: You introduce a new client dependency, demonstrating how to abstract external services and handle HTTP calls. Your code breakdown and notes on httpx provide valuable insights.
- Sub-Dependencies: The auth example showcases the true power of DI, allowing for fine-grained authorization logic. The flow diagram clarifies the hierarchy.
Testing Power Unleashed
- conftest.py and Fixtures: You explain pytest conventions and the importance of the conftest.py module.
- Dependency Overrides: The step-by-step breakdown of the client fixture is brilliant. It shows how to swap dependencies for testing seamlessly and highlights the flexibility FastAPI offers.
- Clean-up: Emphasizing the yield pattern and clean-up in the client fixture reinforces good testing practices.
- Beyond HTTP: You make a crucial point that DI applies to more than just HTTP interactions, making it versatile for testing various scenarios.