React app with FastAPI, SQLAlchemy, PostgreSQL, and Docker-Compose (a tutorial) [part 2: FastAPI]

This will cover getting our backend API up and running!

If you missed part 1 and need to set up some your initial docker-compose file and directory structure, you can check that out here if you want.

It was either this or a generic photo to act as a metaphor for the word “fast”

FastAPI

Let’s start with FastAPI. FastAPI is a python framework that makes it super easy to stand up a REST API. But we can take a brief moment to evaluate it as a tool.

Pros:

  • OpenAPI compliant (which means free Swagger documentation!)
  • Support for typing (for IDE hints)
  • Fast (per their documentation, as performant as NodeJS- and Go-based backends)

Cons:

  • No native frontend built into the framework (although… I see this as a pro. No hand-waving magic)
  • No native object-relational mapping (ORM) built into the framework either (which something bigger like Django has)

Convinced? Let’s do this!

First, let’s get our python dependencies in order.

# /backend/requirements.txt
fastapi
pydantic
uvicorn

We’ll add to this later when we start needing more in our python environment to interact with a database.

Now let’s make sure that when we start our Docker containers, these dependencies are installed.

# /backend/Dockerfile
from python:3
RUN python -m pip install --upgrade pip
WORKDIR /app# Install python dependencies
COPY ./requirements.txt $WORKDIR
RUN python -m pip install -r ./requirements.txt

And finally, let’s tweak our top-level docker-compose.yml too to map some ports and mount the directory as a volume to give the container access to the files in real time (makes it easier for hot-loading during the development process).

# /docker-compose.yml
version: '3.9'
services:
backend:
build: ./backend
ports:
- '8080:8080'
volumes:
- ./backend:/app
command: ['python', 'main.py']
frontend:
build: ./frontend

Cool, now we have everything we need to start using FastAPI! Let’s start off with a basic set of files, but let’s arrange them such that the files will start off living in their forever homes:

/
docker-compose.yml
frontend/
Dockerfile
backend/
Dockerfile
requirements.txt
main.py
app/
__init__.py
api/
__init__.py
api.py

Okay that file tree may look as though it just exploded with a bunch of new directories and files, but all we really added are main.py and /backend/app/api/api.py . The __init__.py files are both be empty and will remain so.

So what’s in those files?

# /backend/main.py
import uvicorn
if __name__ == '__main__':
uvicorn.run('app.api.api:app', host='0.0.0.0', port=8080, reload=True)

Pretty simple, right? To quickly break this down, we are importing uvicorn which is one of the dependencies we installed. It’s what FastAPI uses as its ASGI implementation, which just means that it’s setting us up to be a web server. It’ll handle receiving the HTTP requests on the specified port (8080) and send the traffic to our FastAPI app that lives in the file at the relative location of app.api.api . Which we just created! Let’s see what’s in that file.

# /backend/app/api/api.py
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()origins = [
'http://localhost',
'localhost'
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)

@app.get("/")
async def root():
return {"message": "Hello World"}

Okay there’s a lot here, but the majority of it is setting up Cross-Origin Resource Sharing (CORS) which will basically allow us to set up our React frontend to talk to this backend. Currently, though, we are only allowing traffic in from our local machine.

The key lines are where we first initialize our FastAPI object, just under the imports, and where we set up our first API endpoint (last few lines). FastAPI uses decorators (similar to Flask) to assign routes to chunks of code.

Taking a deeper look:

@app.get("/")
async def root():
return {"message": "Hello World"}

We see that the route we are declaring here is the root path "/" . We give its corresponding function the name root and also declare it to be async (something ASGI allows). We then return a dictionary that FastAPI will serialize for us to the client.

Now, because FastAPI is OpenAPI compliant, we can actually view and test this endpoint in our browser!

From the root directory, let’s run:

# From /
docker-compose build && docker-compose up -d

This will build our images and start the containers as declared in our docker-compose file. On startup, our backend container will run python main.py from our $WORKDIR, where we are mounting our local /backend/ directory.

Once it’s done building and the container is up and running (you can run docker-compose logs -f backend to check the status of the container), open a browser and navigate to localhost:8080.

And boom! You should see something similar to the following:

OpenAPI Documentation! For free!

If you click “Try it out”, you can actually hit the endpoint you just made and get that sweet sweet Hello World response!

Okay “advanced” may be a misnomer, but I did want to add one more thing for the pragmatic programmer (ugh I hate that I said that too) that is thinking ahead to “well… am I really going to put all my endpoints into api.py? That feels… wrong. Right??

And with such a someone I would absolutely agree. So let’s add some files.

/
docker-compose.yml
frontend/
Dockerfile
backend/
Dockerfile
requirements.txt
main.py
app/
__init__.py
api/
__init__.py
api.py
routers/
__init__.py
books.py

The naming of these files provides some foreshadowing of what our final product will be. But! Let’s not dwell on that. Let’s instead see what is inside books.py.

from fastapi import APIRouter
from pydantic import BaseModel
from typing import List
router = APIRouter(
prefix='/api/books'
)
class Book(BaseModel):
title: str
author: str
all_books = [
Book(title='Normal People', author='Sally Rooney'),
Book(title='The Souls of Yellow Folk', author='Wesley Yang')
]
@router.get('/', response_model=List[Book])
async def get_books():
'''
Retrieve all books.
'''
return all_books
@router.get('/list/', response_model=List[str])
async def get_book_titles():
'''
Retrieve all book titles.
'''
return [book.title for book in all_books]

Here, you’ll see a lot of things similar to what we already had in api.py: a top-level declaration of a fastapi object, path operator decorators that use that object, and some endpoint functions.

Briefly, here’s what we have:

  • APIRouter. This is functionally the same as the FastAPI object in api.py but can be modularly added to that first object! So, we can create arbitrarily many of these router objects and consolidate them all into api.py , which I’ll show shortly. You’ll also notice the prefix keyword, which specifies a base path for all subsequent path operators, which will be relative to this prefix.
  • The Book class. This is just a hardcoded class that will later be supplanted by database objects. It extends the pydantic BaseModel simply for typing.
  • all_books. Just a hardcoded list of Books.
  • get_books() and get_book_titles(). These, like our Hello World function, return information that FastAPI will serialize for us. The former will return our list of Books, the latter just the titles of those books. In the decorators for each of these functions, we declare the relative path (again, relative to the APIRouterprefix) and the return type.

Before we can test this out, we need to change one more thing, this time in api.py.

# /backend/app/api/api.py
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routers import books # <- New!app = FastAPI()origins = [
'http://localhost',
'localhost'
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
app.include_router(books.router) # <- New!

You’ll see I took out the Hello World endpoint and instead imported our new file and from that file, included the router in our FastAPI object. This will include our new endpoints!

Now, if you are mounting the directory as a volume, you should be able to just reload localhost:8080/docs.

And the endpoints work! Notice the relative path (/api/books/) even though the decorator simply had “/”. Neat!

Now, if only we had real data to return here… but that will be next time in Part 3. Stay tuned!

Software Engineer @ HMS