top of page

FastAPI: The next steps

Writer: Mic
Mic
May 22
6 min read

The last post showed how FastAPI uses types to eliminate boilerplate and how it handles validations. That was the foundation, while in this post is about composition: authentication, background work, databases, errors, and making it all testable.

These are the patterns that can upgrade the examples from the previous post to a more involved system.

Uh, the machine got more complicated this time...

Authentication via JWT

Most API authentication work in the following pattern:

  1. User logs in with credentials

  2. Server returns a JWT (JSON Web Token) token

  3. Client includes token in subsequent requests

  4. Server validates token and extracts user info

An implementation could look like this

This example shows a complete, minimal JWT-based login flow in FastAPI. The configuration section defines the secret key used to sign tokens, the signing algorithm, and how long access tokens remain valid.

After FastAPI creates the application, PasswordHash.recommended() sets up secure password hashing, and OAuth2PasswordBearer(tokenUrl="token") tells FastAPI that protected routes expect a bearer token that can be obtained from the /token endpoint.

The Pydantic models describe the shapes of the data used by the API. Token defines the response returned after login, TokenData represents information extracted from a decoded token, User describes the public user data returned by the API, and UserInDB extends that with the hashed password stored internally.

The fake_users_db dictionary acts as a simple mock database, storing one user called john with a securely hashed version of the password "secret". In a production model, this would of course be replaced by some database access.

The helper functions contain the main authentication logic. First verify_password() checks whether a submitted password matches the stored hash. Then get_user() looks up a user in the fake database and converts the dictionary into a UserInDB object., essentially adding the hashed password. The function authenticate_user() combines these two steps: it finds the user and verifies the password, returning None if either step fails. Finally create_access_token() builds a JWT by copying the supplied data, adding an expiry time, and signing it with the secret key.

The get_current_user() function protects private routes. FastAPI automatically extracts the bearer token from the Authorization header, decodes it, checks that it contains a username in the "sub" field, and then looks up that user in the database. If the token is missing, invalid, expired, or refers to a user that does not exist, it raises a 401 Unauthorized error.

The two endpoints show how the pieces fit together. We have a POST /token receives login details from an OAuth2 password form, authenticates the user, creates a short-lived access token, and returns it to the client. In our case of course we just use the curl command to give the result of such a form. Then we have the GET /users/me endpoint: it depends on get_current_user(), so it only runs if the request includes a valid bearer token. If authentication succeeds, it returns the current user’s public details.

This is then used via:

# Login
curl -X POST http://127.0.0.1:8000/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=john&password=secret"

# Returns: {"access_token": "eyJ0eXAi...", "token_type": "bearer"}

# Access protected endpoint
curl http://127.0.0.1:8000/users/me \
  -H "Authorization: Bearer eyJ0eXAi..."

Any other endpoint that needs authentication can now just adds current_user: User = Depends(get_current_user) to its signature as a dependency.


Background Tasks: Work That Does Not Block

Some operations should not hold up the response:

  • Sending emails

  • Processing uploaded files

  • Logging analytics events

  • Generating reports

FastAPI has built-in background tasks for simple cases:

The response returns immediately, but the send_email function call is added to the background task list.

For a full application, one would consider a proper task queue. This could for example be done with Celery:

Now the task runs in a separate worker process, especially it is independent from the server and would still be active after a crash and a possible restarts. You can monitor it. You can retry it. To run all of this, you have to add a few things. For example via

brew install redis
brew services start redis

Then start the Celery worker

celery -A main.celery_app worker --loglevel=info

This worker than runs even if the FastAPI server has to be restarted. Finally, start the FastAPI server as usual.

When to use which:

  • Built-in BackgroundTasks: Fire-and-forget operations that finish quickly

  • Celery/RQ/Dramatiq: Long-running tasks, task chains, retries, monitoring


Database Integration with SQLAlchemy Async

FastAPI works well with async database drivers. We will not go into every detail, but below is a short basic setup with SQLAlchemy 2.0. It is fairly easy to follow, apart from a single dependency, it only has three simple endpoint for creating an item, reading an item, and reading all items.

The key patterns here are:

  • Database session created per request via dependency for each of the endpoints

  • Async context manager ensures session cleanup

  • Pydantic models for API, SQLAlchemy models for database

  • model_config = ConfigDict(from_attributes=True) lets Pydantic read from ORM objects, in older versions of Pydantic this would be handled via a Config class.

Since the connections are pooled, we should handle errors from integrity and roll back the database:

Of course, this is still a baby example. To push this further we would have to add a lot more, but the basic aspects are the same.


Error Handling: Custom Exceptions and Handlers

FastAPI’s default error responses are good, but in many cases you can make them better by defining custom exceptions.

Then we have to add exception handlers:

Then an endpoint can raise the proper exceptions:

The error response is consistent and informative and shows the content that was defines. For validation errors, you can customise those too:


Testing without a Server

FastAPI has excellent testing support, wihtout the need for a running server.

This is tests a variety of the functions we had before for both authentication and database usage. It can then be run using pytest. Note that you will most likely also have to use the package HTTPX.

For async tests with a real database, use httpx.AsyncClient:

In addition, we do not want to use our original database for testing, but a specific one for testing. In this case we need to override the dependency

where create_test_db_session() is a stand-in function for creating a testing database.


Middleware: Cross-Cutting Concerns

Middleware handles things that affect multiple endpoints, this includes aspects like logging, CORS (Cross-Origin Resource Sharing), compression, rate limiting.

CORS for frontend apps:

This tells FastAPI:

Allow browser requests from http://localhost:3000
Allow cookies/auth headers
Allow all HTTP methods: GET, POST, PUT, DELETE, etc.
Allow all headers

Request logging:

Every time a request comes in, this logging function is run. It measures the time around the actual request and then prints it as well as adding a custom header to the response with the time it took. This can then be used at other spots.

Rate limiting with slowapi:

This uses the slowapi package to rate limit a FastAPI endpoint. It sets up a Limiter object which is linked to the callers IP address. Then the limiter is attached to the app and the app is told what to do when the limiter is exceeded. The endpoint is then attached to 5 requests per minute.

A final piece that is important here, is that we pass the Request item to the endpoint even if we do not use it, otherwise slowapi cannot hook into it to use the function used to group the requests for the Limiter.


Project Structure: Beyond One File

For real applications, split the code:

project/
├── app/
│   ├── __init__.py
│   ├── main.py              # FastAPI app instance
│   ├── config.py            # Settings and configuration
│   ├── dependencies.py      # Shared dependencies
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py          # SQLAlchemy models
│   │   └── item.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── user.py          # Pydantic models
│   │   └── item.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── users.py         # User endpoints
│   │   └── items.py         # Item endpoints
│   └── services/
│       ├── __init__.py
│       ├── auth.py          # Authentication logic
│       └── email.py         # Email sending
├── tests/
│   ├── __init__.py
│   ├── test_users.py
│   └── test_items.py
├── requirements.txt
└── .env

The main app file becomes minimal:

Each router is self-contained and then uses the router instead of the app before:


When FastAPI Is Not Enough

FastAPI is excellent for most APIs. But:

Consider Django REST Framework if:

  • You need Django’s admin panel

  • You want more built-in authentication schemes

  • Your team already knows Django

  • You need class-based views

Consider Flask if:

  • You want fewer dependencies

  • You are building something small

  • You do not need auto-generated docs

  • You prefer explicit over automatic

Consider gRPC/GraphQL if:

  • Clients need to request specific fields

  • You are building microservices that talk to each other

  • REST semantics do not fit your use case

FastAPI works best when:

  • You are building a modern JSON API

  • You use Python type hints

  • You want validation and docs for free

  • You might need async later

  • You value developer experience


Conclusion

The fundamentals from the first post, namely declare what you want, let the framework handle mechanics, extends to everything here. Additions like authentication or database sessions are dependencies, background tasks are a parameter when using FastAPI directly, and errors are exceptions with handlers.

You compose these pieces as you need them and the framework connects them. This is why FastAPI feels different to many other frameworks. It is not a collection of utilities, rather it is a system built around a single idea: use Python’s type system to eliminate boilerplate and make the right thing easy.

Comments


bottom of page