DEV Community

Cover image for FastAPI Best Practices: A Condensed Guide with Examples
Developer Service
Developer Service

Posted on • Originally published at developer-service.blog

FastAPI Best Practices: A Condensed Guide with Examples

FastAPI is a modern, high-performance web framework for building APIs with Python, based on standard Python type hints.

It was designed to be fast, easy to use, and highly compatible with other web frameworks and tools.

FastAPI leverages the power of async/await and Python 3.7+ type hints to provide an efficient and developer-friendly experience for building web applications and APIs.


Key Features and Importance of Best Pratices

Some key features of FastAPI include:

  • High performance: FastAPI is built on top of Starlette and Pydantic, which makes it one of the fastest web frameworks available for Python.
  • Easy to learn and use: FastAPI is designed to be simple and intuitive, making it easy for developers to get started and be productive quickly.
  • Automatic documentation: FastAPI automatically generates interactive API documentation using OpenAPI (formerly Swagger) and JSON Schema.
  • Type safety: FastAPI enforces type safety using Python type hints, which helps catch bugs and errors at development time rather than runtime.
  • Async/await support: FastAPI supports asynchronous programming, allowing developers to build highly concurrent and scalable applications.

Following best practices when working with FastAPI is crucial for several reasons:

  • Maintainability: Adhering to best practices ensures that your code is well-organized, easy to understand, and maintainable, making it simpler for other developers to contribute to your project.
  • Scalability: Implementing best practices helps you create a solid foundation for your application, allowing it to scale efficiently as it grows in complexity and size.
  • Security: Following best practices can help you avoid common security pitfalls and vulnerabilities, ensuring that your application is more secure and robust.
  • Performance: By adhering to best practices, you can optimize your application's performance, taking full advantage of FastAPI's speed and efficiency.
  • Community standards: Following best practices aligns your code with community standards and expectations, making it easier for others to collaborate, review, and provide feedback on your work.

Setting Up and Structuring Your FastAPI Project

Before starting your FastAPI project, it's essential to set up a virtual environment to isolate your project's dependencies from other Python projects on your system.

You can use tools like venv or pipenv to create a virtual environment. In this example, we'll use venv:

  • Create a new directory for your project and navigate to it in your terminal.
  • Create a virtual environment using the following command:
python3 -m venv venv
Enter fullscreen mode Exit fullscreen mode

This command creates a new virtual environment named venv in your project directory.

  • Activate the virtual environment. The activation command depends on your operating system:

For Linux and macOS:

source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

For Windows:

venv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

With the virtual environment activated, install FastAPI and an ASGI server like Uvicorn or Hypercorn. In this example, we'll use Uvicorn:

pip install fastapi uvicorn
Enter fullscreen mode Exit fullscreen mode

Structuring your project for scalability

Structuring your FastAPI project properly is crucial for maintaining a scalable and maintainable codebase.

Here's a suggested project structure that you can adapt to your needs:

my_fastapi_project/
│
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── v1/
│   │   │   ├── __init__.py
│   │   │   ├── users.py
│   │   │   ├── posts.py
│   │   │   └── ...
│   │   └── v2/
│   │       └── ...
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── post.py
│   │   └── ...
│   ├── config.py
│   ├── database.py
│   └── utils.py
│
├── tests/
│   ├── __init__.py
│   ├── test_users.py
│   ├── test_posts.py
│   └── ...
│
├── .env
├── requirements.txt
├── Dockerfile
└── README.md

Enter fullscreen mode Exit fullscreen mode

Key components of the project structure:

  • app/: Contains the main application logic, including the API routes, models, configuration, database, and utility functions.
  • app/main.py: The entry point for your FastAPI application, where you initialize the app and mount the API routes.
  • app/api/: Contains the API routes organized by version (e.g., v1, v2).
  • app/models/: Contains the Pydantic data models used for data validation and serialization.
  • app/config.py: Contains the application configuration settings.
  • app/database.py: Contains the database connection and related functions.
  • app/utils.py: Contains utility functions used throughout the application.
  • tests/: Contains the unit tests for your application. .env: Contains environment variables for your application.
  • requirements.txt: Lists the Python dependencies for your application.
  • Dockerfile: Used for building a Docker image of your application.
  • README.md: Provides an overview and documentation for your project.

Data Validation and Handling HTTP Requests

FastAPI uses Pydantic for data validation and serialization.
Pydantic enforces type safety and provides helpful error messages when invalid data is submitted.

To define a data model with Pydantic, create a class with the required attributes and their corresponding types:

from pydantic import BaseModel

class UserIn(BaseModel):
    username: str
    email: str
    password: str

class UserOut(BaseModel):
    id: int
    username: str
    email: str
    class Config:
        orm_mode = True
Enter fullscreen mode Exit fullscreen mode

In this example, we define two Pydantic models, UserIn and UserOut, for handling input and output user data, respectively.

Implementing path and query parameters

FastAPI allows you to define path and query parameters for your API endpoints.

Path parameters are part of the URL path, while query parameters are added to the URL as key-value pairs.
Example:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    return {"item_id": item_id, "item": item}

Enter fullscreen mode Exit fullscreen mode

In this example, we define two API endpoints:

  • /items/{item_id}: A GET endpoint that accepts an item_id path parameter and an optional q query parameter.
  • /items/{item_id}: A PUT endpoint that accepts an item_id path parameter and a Pydantic Item model as the request body.

Handling HTTP requests and responses

FastAPI simplifies handling HTTP requests and responses.

You can define the required HTTP method (e.g., GET, POST, PUT, DELETE) for each endpoint, set status codes, and return responses with custom headers.

Example:

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

app = FastAPI()

@app.get("/users", response_model=List[UserOut])
async def get_users():
    users = await get_users_from_db()
    return users

@app.post("/users", response_model=UserOut, status_code=201)
async def create_user(user: UserIn):
    user_id = await create_user_in_db(user)
    return await get_user_by_id_from_db(user_id)

@app.delete("/users/{user_id}", response_class=JSONResponse)
async def delete_user(user_id: int):
    result = await delete_user_from_db(user_id)
    if not result:
        raise HTTPException(status_code=404, detail="User not found")
    return {"detail": "User deleted"}
Enter fullscreen mode Exit fullscreen mode

In this example, we define three API endpoints:

  • /users: A GET endpoint that returns a list of users with a 200 OK status code.
  • /users: A POST endpoint that creates a new user and returns the created user with a 201 Created status code.
  • /users/{user_id}: A DELETE endpoint that deletes a user and returns a custom response with a 200 OK status code. If the user is not found, it raises an HTTPException with a 404 Not Found status code.

Exception Handling, Middleware, and CORS

FastAPI provides built-in support for exception and error handling.

You can define custom exception handlers to centralize the way your application handles specific exceptions, and you can set HTTP error responses for different status codes.

Example:

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return JSONResponse(
        status_code=422,
        content={"detail": "Validation error", "errors": exc.errors()}
    )

@app.response_exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    return JSONResponse(
        status_code=exc.status_code,
        content={"detail": exc.detail}
    )

Enter fullscreen mode Exit fullscreen mode

In this example, we define two custom exception handlers:

  • validation_exception_handler: Handles RequestValidationError exceptions and returns a 422 Unprocessable Entity status code along with the validation errors.
  • http_exception_handler: Handles HTTPException exceptions and returns the corresponding status code and error detail.

Creating and registering custom middleware

FastAPI allows you to create and register custom middleware to perform actions before or after processing a request.

Middleware is useful for tasks like logging, authentication, and rate limiting.

Example:

from fastapi import FastAPI, Request

app = FastAPI()

async def log_request_middleware(request: Request, call_next):
    request_start_time = time.monotonic()
    response = await call_next(request)
    request_duration = time.monotonic() - request_start_time
    log_data = {
        "method": request.method,
        "path": request.url.path,
        "duration": request_duration
    }
    log.info(log_data)
    return response

app.middleware("http")(log_request_middleware)
Enter fullscreen mode Exit fullscreen mode

In this example, we define a custom middleware called log_request_middleware.

This middleware logs the request method, path, and duration for each incoming request.

The middleware is then registered with the FastAPI app using app.middleware().

Enabling Cross-Origin Resource Sharing (CORS)

FastAPI provides built-in support for Cross-Origin Resource Sharing (CORS), which allows you to control which domains can access your API.

Enabling CORS is essential for modern web applications that rely on APIs to fetch and manipulate data.

Example:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
    "http://localhost",
    "http://localhost:8080",
    "https://example.com"
]

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

Enter fullscreen mode Exit fullscreen mode

In this example, we enable CORS for the FastAPI app by adding the CORSMiddleware middleware.

The allow_origins parameter specifies a list of domains that are allowed to access the API.

The allow_credentials, allow_methods, and allow_headers parameters configure additional CORS settings.


Testing, Debugging, and Deployment

Testing and debugging are essential parts of the software development process.

FastAPI provides built-in support for testing and debugging, making it easier to ensure the quality and reliability of your
application.

Example of Testing with FastAPI:

# tests/test_users.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_user():
    user_data = {"username": "testuser", "email": "test@example.com", "password": "test123"}
    response = client.post("/users", json=user_data)
    assert response.status_code == 201
    assert response.json()["username"] == user_data["username"]
    assert response.json()["email"] == user_data["email"]

def test_get_users():
    response = client.get("/users")
    assert response.status_code == 200
    assert len(response.json()) >= 1
Enter fullscreen mode Exit fullscreen mode

In this example, we create two test cases for the /users endpoint using FastAPI's TestClient.

The test_create_user function tests the creation of a new user, while the test_get_users function tests the retrieval of a list of users.

Debugging with the interactive debugger

To enable the interactive debugger in FastAPI, you can use the --reload and --debug flags when running the application with Uvicorn:

uvicorn app.main:app --reload --debug
Enter fullscreen mode Exit fullscreen mode

With these flags enabled, the application will automatically reload on code changes, and the interactive debugger will be activated when an unhandled exception occurs.

Deployment and scaling considerations

When deploying a FastAPI application, it's essential to consider factors like performance, scalability, and security.

Some best practices for deploying and scaling FastAPI applications include:

  • Using a production-ready ASGI server like Gunicorn or Hypercorn.
  • Configuring a reverse proxy like Nginx to handle SSL termination, static file serving, and request routing.
  • Using a process manager like systemd or supervisor to manage and monitor your application.
  • Implementing caching and/or using a content delivery network (CDN) to improve performance.
  • Horizontally scaling your application by running multiple instances behind a load balancer.
  • Deploying your FastAPI application with Gunicorn and Nginx Gunicorn and Nginx are popular choices for deploying FastAPI applications in production.

Gunicorn is a Python WSGI HTTP Server for UNIX, while Nginx is a high-performance, open-source web server and reverse proxy.
Example Gunicorn configuration:

# gunicorn.conf.py
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"
Enter fullscreen mode Exit fullscreen mode

In this example, we create a gunicorn.conf.py file to configure the Gunicorn server.

We set the number of workers to 4 and specify the UvicornWorker class for handling ASGI applications.

Example Nginx configuration:

# nginx.conf
server {
    listen 80;
    server_name example.com;
    location / {
        include uwsgi_params;
        uwsgi_pass unix:/path/to/your/project/my_fastapi_project.sock;
    }
}

Enter fullscreen mode Exit fullscreen mode

In this example, we create an nginx.conf file to configure the Nginx server.

We set the server to listen on port 80, specify the domain name, and configure the / location to proxy.


Conclusion

This guide presented essential best practices for building FastAPI applications.

By setting up and structuring your project for scalability, using Pydantic for data validation, handling HTTP requests and responses, implementing exception and error handling, custom middleware, and CORS, and testing and debugging your application, you can create robust and maintainable FastAPI applications.

Deploying your application with Gunicorn and Nginx ensures improved performance and scalability.

To stay updated with the latest FastAPI features and best practices, continue learning and engaging with the FastAPI community, exploring open-source projects, and experimenting with new tools and techniques.

Following these best practices will help you build high-quality APIs that meet your users' needs and support your development goals.

Top comments (1)

Collapse
 
devasservice profile image
Developer Service

Hello everyone, hope you like my post.

I wrote this post because I was learning more about Pydantic and even wrote an article about it.

I have been using FastAPI in side projects for a while, but I never really thought about how a project should be structured and what principles it should have/follow.

Feedback is appreciated.

Available to answer any questions.