The Ultimate FastAPI Tutorial Part 8 - Project Structure, Settings and API Versioning
In part 8 of the FastAPI tutorial, we'll look at versioning our API
Welcome to the Ultimate FastAPI tutorial series. This post is part 8. The series is a project-based tutorial where we will build a cooking recipe API. Each post gradually adds more complex functionality, showcasing the capabilities of FastAPI, ending with a realistic, production-ready API. The series is designed to be followed in order, but if you already know FastAPI you can jump to the relevant part.
Tutorial Series Contents
Beginner Level Difficulty
Intermediate Level Difficulty
Part 14: Using Docker and Uvicorn to Deploy Our App to IaaS (Coming soon)
Part 15: Exploring the Open Source Starlette Toolbox - GraphQL (Coming soon)
Part 16: Alternative Backend/Python Framework Comparisons (i.e. Django) (Coming soon)
This is a more lightweight post compared the beast that is part 8 where we looked at database setup. Nonetheless, by structuring your FastAPI projects well, you’ll set your REST APIs up for easy extensibility and maintenance later.
This is post borrows heavily from the official full-stack FastAPI postgresql cookie-cutter repo. For learning, the cookie cutter repo is a bit complex, so we’re simplifying things at this point in the series. However, by the end of the tutorial we’ll have something similar.
Practical Section 1 - FastAPI Project Structure and Config
Let’s take a look at the new additions to the app directory:
├── app │ ├── __init__.py │ ├── api ----> NEW │ │ ├── __init__.py │ │ ├── api_v1 ----> NEW │ │ │ ├── __init__.py │ │ │ ├── api.py ----> NEW │ │ │ └── endpoints ----> NEW │ │ │ ├── __init__.py │ │ │ └── recipe.py ----> NEW │ │ └── deps.py │ ├── backend_pre_start.py │ ├── core ----> NEW │ │ ├── __init__.py │ │ └── config.py ----> NEW │ ├── 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 ----> UPDATED │ ├── 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
As you can see, we’ve added a new
api directory. Our purpose here is to unclutter the
and allow for API versioning, we’ll look at that in the second (versioning) part of this blog post.
We’ve also now added the
core/config.py module, which is a standard FastAPI structure. We use
Pydantic models in here (as we do for the schemas) to define the app config. This allows us to make use of
Pydantics type inference and validators. Let’s look at the
core/config.py code to illustrate:
Settingsclass inherits from the Pydantic
BaseSettingsclass. This model will attempt to determine the values of any fields not passed as keyword arguments by reading from environment variables of the same name. This is why you won’t see code like
API_V1_STR: str = os.environ['API_V1_STR']because it’s already doing that under the hood.
- As with other Pydantic models, we use type hints to validate the config - this can save us from a lot of errors as config code is notoriously poorly tested.
- Using Pydantic validator decorators it’s possible to validate config fields using functions.
- Behaviour of pydantic can be controlled via the Config class on a model, in this example we specify that our settings are case-sensitive.
- Finally we instantiate the
Settingsclass so that
app.core.config.settingscan be imported throughout the project.
You’ll see that the code for this part of the tutorial has now been updated so that all significant
global variables are in the config (e.g.
As the project grows, so too will the complexity of the config (we’ll see this soon enough in future parts of the tutorial). This is a useful starting point with enough realism to give a feel for what could be here.
Practical Section 2 - API Versioning
It is best practice to version your APIs. This allows you to manage breaking API changes with your clients in a more disciplined and structured way. The Stripe API is the gold standard for this, if you’d like some inspiration.
Let’s start by observing the new API versioning introduced in this part of the tutorial:
- Clone the tutorial project repo
pip install poetry(if you don’t have it already)
poetry run ./prestart.sh(sets up a new DB in this directory)
poetry run ./run.sh
- Open http://localhost:8001
You should be greeted by our usual server-side rendered HTML:
So far no change. Now navigate to the interactive UI docs at
notice that the recipe endpoints now are prefaced with
Go ahead and have a play with the endpoints (they should all work exactly the same as the previous part of the tutorial). We now have versioning. Let’s look at the code changes which have led to this improvement:
Notice how the recipe endpoint logic is pulled in from
we have extracted the recipe endpoint code from
app/main.py). We then use the the
method, passing in a prefix of
/recipes. This means that endpoints defined in the
which specify a route of
/ will be prefixed by
Then back in
app/main.py we continue to stack the FastAPI routers:
Once again we use the
prefix argument, this time with the
API_V1_STR from our config. In short,
we stack prefixes of
api.py). This creates the versioned
routes we see in the documentation UI.
Now whenever we want to add new logic (e.g. a users API), we can simply define a new module in
If we want to create a v2 API, we have a structure that allows for that.
The other point to note from the above code snippet is that because we do not apply any versioning prefix to our root route (the home route Jinja template), then this one endpoint is not versioned.
Continue Learning FastAPI
OK, that one was a simpler palate cleanser before we start to dig more into complexity. In the next post we’re going to look at how FastAPI makes use of Python’s asyncio library to deliver impressive performance.