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

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!

Code: Environment

# /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

Code: The Actual FastAPI Stuff

/
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.

Code: Demo

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!

Advanced Code: Routing

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!

Advanced Code: Demo

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store