Early bird orders for the course, "FastAPI for Busy Engineers" are available at a discount until April 27th.

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
Created: 01 August 2021
Last updated: 01 August 2021

Introduction

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.

Code

Project github repo directory for this part of the tutorial

Tutorial Series Contents

Optional Preamble: FastAPI vs. Flask

Beginner Level Difficulty

Part 1: Hello World
Part 2: URL Path Parameters & Type Hints
Part 3: Query Parameters
Part 4: Pydantic Schemas & Data Validation
Part 5: Basic Error Handling
Part 6: Jinja Templates
Part 6b: Basic FastAPI App Deployment on Linode

Intermediate Level Difficulty

Part 7: Setting up a Database with SQLAlchemy and its ORM
Part 8: Production app structure and API versioning
Part 9: Creating High Performance Asynchronous Logic via async def and await
Part 10: Authentication via JWT
Part 11: Dependency Injection and FastAPI Depends
Part 12: Setting Up A React Frontend
Part 13: Using Docker, Uvicorn and Gunicorn to Deploy Our App to Heroku
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)

Post Contents

Practical Section 1 - FastAPI Project Structure and Config
Practical Section 2 - API Versioning

FastAPI logo


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 main.py file 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:

from pydantic import AnyHttpUrl, BaseSettings, EmailStr, validator
from typing import List, Optional, Union


class Settings(BaseSettings):  # 1
    API_V1_STR: str = "/api/v1"  # 2
    # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
    # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
    # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
    BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []

    @validator("BACKEND_CORS_ORIGINS", pre=True)  # 3
    def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
        if isinstance(v, str) and not v.startswith("["):
            return [i.strip() for i in v.split(",")]
        elif isinstance(v, (list, str)):
            return v
        raise ValueError(v)

    SQLALCHEMY_DATABASE_URI: Optional[str] = "sqlite:///example.db"
    FIRST_SUPERUSER: EmailStr = "admin@recipeapi.com"

    class Config:
        case_sensitive = True  # 4


settings = Settings()  # 5
  1. The Settings class inherits from the Pydantic BaseSettings class. 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.
  2. 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.
  3. Using Pydantic validator decorators it’s possible to validate config fields using functions.
  4. Behaviour of pydantic can be controlled via the Config class on a model, in this example we specify that our settings are case-sensitive.
  5. Finally we instantiate the Settings class so that app.core.config.settings can 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.SQLALCHEMY_DATABASE_URI, FIRST_SUPERUSER).

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
  • cd into part-8
  • pip install poetry (if you don’t have it already)
  • poetry install
  • 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:

Root route

So far no change. Now navigate to the interactive UI docs at http://localhost:8001/docs. You’ll notice that the recipe endpoints now are prefaced with /api/v1:

Versioned FastAPI endpoints

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:

app/api_v1/api.py

from fastapi import APIRouter

from app.api.api_v1.endpoints import recipe


api_router = APIRouter()
api_router.include_router(recipe.router, prefix="/recipes", tags=["recipes"])

Notice how the recipe endpoint logic is pulled in from app/api.api_v1.endpoints.recipe.py (where we have extracted the recipe endpoint code from app/main.py). We then use the the include_router method, passing in a prefix of /recipes. This means that endpoints defined in the recipes.py file which specify a route of / will be prefixed by /recipes.

Then back in app/main.py we continue to stack the FastAPI routers:

# skipping...

root_router = APIRouter()
app = FastAPI(title="Recipe API", openapi_url="/openapi.json")


@root_router.get("/", status_code=200)
def root(
    request: Request,
    db: Session = Depends(deps.get_db),
) -> dict:
    """
    Root GET
    """
    recipes = crud.recipe.get_multi(db=db, limit=10)
    return TEMPLATES.TemplateResponse(
        "index.html",
        {"request": request, "recipes": recipes},
    )


app.include_router(api_router, prefix=settings.API_V1_STR)  # <----- API versioning
app.include_router(root_router)

# skipping...

Once again we use the prefix argument, this time with the API_V1_STR from our config. In short, we stack prefixes of /api/v1 (from main.py) then recipes (from 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 app/api/api_v1/endpoints. 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.

Continue to part 9

Category