REST APIs with FastAPI

REST APIs with FastAPI

Welcome back, Python enthusiast! Today, we're diving into one of the most exciting modern frameworks for building APIs: FastAPI. If you've been looking for a way to create high-performance, easy-to-write, and automatically documented APIs, you're in the right place. FastAPI is built on standard Python type hints, and it leverages Pydantic for data validation and Starlette for web handling. The result? An incredibly fast and developer-friendly experience.

Why FastAPI?

You might be wondering why you should choose FastAPI over other frameworks like Flask or Django REST Framework. The answer lies in its speed, simplicity, and out-of-the-box features. FastAPI is one of the fastest Python frameworks available, thanks to its asynchronous capabilities. It also provides automatic interactive API documentation, which is a huge time-saver during development and testing.

Here's a quick comparison of request handling speeds among popular Python web frameworks (requests per second):

Framework Requests per Second
FastAPI 5,900
Flask 1,900
Django 1,400
Tornado 3,200

As you can see, FastAPI outperforms many alternatives, making it an excellent choice for high-throughput applications.

Getting Started with FastAPI

Let's jump right in and create your first FastAPI application. First, you'll need to install FastAPI and an ASGI server, such as Uvicorn. You can do this using pip:

pip install fastapi uvicorn

Now, create a new Python file (e.g., main.py) and add the following code:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

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

This simple example defines two endpoints: a root endpoint that returns a JSON response and an endpoint that takes a path parameter (item_id) and an optional query parameter (q).

To run your application, use the following command:

uvicorn main:app --reload

The --reload flag enables auto-reload so your server restarts whenever you make changes to the code. Now, open your browser and go to http://localhost:8000. You should see your {"Hello": "World"} response. Even better, navigate to http://localhost:8000/docs to see the automatically generated interactive API documentation—Swagger UI!

Path Parameters and Query Parameters

In FastAPI, you can define path parameters and query parameters directly in your function definitions using type hints. Path parameters are part of the URL path, while query parameters are appended to the URL after a ?.

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"user_id": user_id}

@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

In the first endpoint, user_id is a path parameter. In the second, skip and limit are query parameters with default values.

Request Bodies with Pydantic Models

For more complex data, especially when you need to receive data from the client (e.g., in a POST request), you can use Pydantic models to define the structure of the request body. This is where FastAPI truly shines, as it automatically validates incoming data against your model.

First, define a Pydantic model:

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

Now, use it in your endpoint:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

@app.post("/items/")
def create_item(item: Item):
    return item

When you send a POST request to /items/ with a JSON body, FastAPI will automatically validate the input based on the Item model. If the data doesn't match, it returns a descriptive error.

Handling HTTP Methods

FastAPI supports all standard HTTP methods: GET, POST, PUT, DELETE, etc. You can define endpoints for each method using decorators like @app.post, @app.put, and @app.delete.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

items = []

@app.get("/items")
def get_items():
    return items

@app.post("/items")
def add_item(item: Item):
    items.append(item)
    return item

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    if item_id < len(items):
        items[item_id] = item
        return item
    return {"error": "Item not found"}

@app.delete("/items/{item_id}")
def delete_item(item_id: int):
    if item_id < len(items):
        deleted_item = items.pop(item_id)
        return deleted_item
    return {"error": "Item not found"}

This example demonstrates a simple CRUD (Create, Read, Update, Delete) API for managing a list of items.

Dependency Injection

FastAPI has a powerful dependency injection system that helps you manage shared logic, such as authentication, database sessions, or configuration. Dependencies are reusable components that you can inject into your path operations.

Here's a simple example of a dependency that checks for a query parameter:

from fastapi import FastAPI, Depends

app = FastAPI()

def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
def read_items(commons: dict = Depends(common_parameters)):
    return commons

The common_parameters function is a dependency that extracts query parameters. It's then injected into the read_items endpoint using the Depends function.

Error Handling

FastAPI makes it easy to handle errors and return appropriate HTTP status codes. You can raise HTTPException to return custom error responses.

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}

@app.get("/items/{item_id}")
def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

In this example, if the item_id is not found in the items dictionary, an HTTP 404 error is raised with a custom message.

Middleware

You can add middleware to your FastAPI application to process requests and responses globally. Middleware functions run for every request before reaching the path operation and after generating the response.

Here's an example of adding a custom middleware that logs request processing time:

import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

This middleware calculates the time taken to process each request and adds it as a header in the response.

Testing Your API

Testing is a crucial part of API development. FastAPI provides a TestClient that allows you to test your application without running a server.

First, install pytest and requests:

pip install pytest requests

Now, create a test file (e.g., test_main.py):

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"Hello": "World"}

def test_read_item():
    response = client.get("/items/42?q=test")
    assert response.status_code == 200
    assert response.json() == {"item_id": 42, "q": "test"}

Run your tests with:

pytest

Deploying Your FastAPI Application

Once your API is ready, you'll want to deploy it. FastAPI applications can be deployed using any ASGI server, such as Uvicorn or Hypercorn. For production, it's recommended to use a process manager like Gunicorn with Uvicorn workers.

Install Gunicorn and Uvicorn:

pip install gunicorn uvicorn

Then, run your application with:

gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app

This command starts Gunicorn with 4 worker processes, each running an instance of your FastAPI app.

Best Practices for FastAPI Development

To make the most of FastAPI, follow these best practices:

  • Use Pydantic models for all request and response bodies to leverage automatic validation and documentation.
  • Take advantage of FastAPI's dependency injection to keep your code clean and reusable.
  • Write tests for your endpoints to ensure reliability.
  • Use async and await for I/O-bound operations to improve performance.
  • Secure your API by implementing authentication and authorization, such as OAuth2 with JWT tokens.

Here's a quick example of implementing OAuth2 password flow:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None

def fake_decode_token(token):
    return User(username=token + "faked", email="john@example.com", full_name="John Doe")

async def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    return user

@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user

This is a simplified example. In a real application, you would verify the token against a database or auth service.

Advanced Features

FastAPI offers many advanced features that we haven't covered, such as:

  • Background tasks: Execute tasks after returning a response.
  • WebSocket support: Build real-time applications.
  • GraphQL integration: Use Strawberry or Ariel to add GraphQL endpoints.
  • Custom middleware: Implement advanced request/response processing.
  • API versioning: Manage different versions of your API gracefully.

Here's an example of a background task:

from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def write_log(message: str):
    with open("log.txt", mode="a") as log:
        log.write(message)

@app.post("/send-notification/{email}")
def send_notification(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_log, f"notification sent to {email}")
    return {"message": "Notification sent in the background"}

This endpoint immediately returns a response while the log writing happens in the background.

Performance Tips

To ensure your FastAPI application runs as efficiently as possible, consider these tips:

  • Use async database drivers (e.g., asyncpg for PostgreSQL) to avoid blocking I/O.
  • Limit the number of middleware functions, as each adds overhead.
  • Enable compression for responses to reduce bandwidth usage.
  • Use a CDN for static files to offload serving from your application.
  • Monitor performance with tools like Prometheus and Grafana.

FastAPI is a powerful, modern framework that can help you build robust and high-performance APIs with minimal effort. Its combination of speed, ease of use, and automatic documentation makes it an excellent choice for projects of any size. Happy coding!