Getting Started with Fast-Api 🏎️ and Docker🐳
Tags: python, docker, api, postgres
Published: 2023-02-02 23:07:01


New month, New blog post...

After the overwhelming response to my last post on [Getting Started with Flask and Docker](https://dev.to/ken_mwaura1/getting-started-with-flask-and-docker-3ie8), I decided to write another one.

This time I'm going to show you how to get started with Fast-Api and Docker. Our demo project will be a simple API that allows the user to create and save notes to a Postgres database.

[Fast-Api](https://fastapi.tiangolo.com/) is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints.

It is a relatively new framework that is gaining popularity in the Python community(**over 50k on GithHub**). It is built on top of Starlette (a lightweight ASGI framework/toolkit, which is ideal for building high performance asyncio services) and Pydantic. It is fast, easy to use and easy to learn, making it a perfect choice for building APIs. It also has built-in support for OpenAPI and Swagger.

As the name implies, Fast-Api is fast. It is one of the fastest Python frameworks available. Using Uvicorn as the ASGI server, it is capable of handling over 10,000 requests per second.

## Audience and Objectives 🗣️

This tutorial is for anyone who wants to get started with Fast-Api and Docker. From beginners to intermediate developers.

By the end of this tutorial, you will be able to:

* Create a Fast-Api project.
* Run the project locally.
* Connect to a Postgres database to perform CRUD operations.
* Dockerize the project.
* Commit the code and Push to GitHub.
* Use GitHub Actions as our CI/CD pipeline to test and build Docker image and container.
* Interact with Api via the browser or 3rd Party tools like Postman, Insomnia, etc.
* Optionally create and connect a Vue frontend to the API.

## Prerequisites🧑‍💻

The main prerequisite for this tutorial is a basic understanding of Python and Docker.

Have the following installed on your machine:

* [Python 3.7+](https://www.python.org/downloads/)
* [Git](https://git-scm.com/downloads) installed on your system.
* [Docker](https://docs.docker.com/get-docker/) running on your system.
* [PostgreSQL](https://www.postgresql.org/download/) installed on your system. I recommend using [Docker](https://info.crunchydata.com/blog/easy-postgresql-12-and-pgadmin-4-setup-with-docker) to run the database.
* Terminal or Command Line Interface (CLI) of your choice.
* Node.js and NPM installed on your system (if you want to create a Vue frontend). [Nvm](https://github.com/nvm-sh/nvm) is recommended.

## Project Structure 📘

The project structure will be as follows:

```bash
Fast-Api-example
├── .github
│   └── workflows
│       └── docker-image.yml
|        └── python-app.yml
|
├── src
│   ├── app
│   │   ├── __init__.py
│   │   ├── api
│   │   │   ├── __init__.py
│   │   │   ├── crud.py
│   │   │   ├── models.py
│   │   │   ├── notes.py
│   │   │   └── ping.py
        ├── models.py
│       ├── db.py
│       ├── main.py
    ├── Dockerfile
    ├── .env
├── .gitignore

├── README.md
├── docker-compose.yml
├── requirements.txt

```

Simple architecture diagram:

![Fast-Api-Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8uah04jzxc9t3bhgf85g.png)

## Getting Started 

These instructions will work on most unix/Linux systems. If you are using Windows, you can use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) or [Git Bash](https://gitforwindows.org/).

This tutorial will be broken down into the following sections:

* [Create a Fast-Api Project](#create-a-fast-api-project).
* [Connect to a Postgres Database](#connect-to-a-postgres-database).
* [Dockerize the Project](#dockerize-the-project).
* [Commit the Code and Push to GitHub](#commit-the-code-and-push-to-github).
* [Create a GitHub Action](#create-a-github-action).
* [Conclusion](#conclusion).
* [References](#references).
*

### Create a Fast-Api Project 🆕

Create a new folder and open it in your terminal.

```bash
mkdir Fast-Api-example
cd Fast-Api-example
```

Create a virtual environment and activate it.

```bash
python3 -m venv venv
source venv/bin/activate
```

Install the required dependencies. Save the following in a file called `requirements.txt`.

```bash
pip install fastapi uvicorn python-dotenv psycopg2-binary
```

```bash
pip freeze > requirements.txt
```

Create a new folder `src` inside it file called `main.py` and add the following code.

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
 return {"Hello": "World"}
```

The code is a simple hello world example. It imports the Fast-Api class and creates an instance of it. It then creates a route that returns a simple JSON object.

Run the project locally.

```bash
uvicorn src.main:app --reload --workers 1 --host 0.0.0.0 --port 8002

```

We are using Uvicorn as the ASGI server. It is a lightning-fast ASGI server implementation, using uvloop and httptools.

We are also using the `--reload` flag to enable hot reloading. This means that whenever we make a change to our code, the server will automatically restart. The `--workers 1` flag is used to specify the number of worker processes. The `--host` and `--port` flags are used to specify the host and port to run the server on.

Open your browser and navigate to `http://localhost:8002/`. You should see the following:

![Fast-Api Hello World](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ro4io9uujmvp5ijhk1w0.png)

### Connect to a Postgres Database

Inside the `src` folder, create a new file called `db.py` and add the following code.

```python
# src/db.py

import os

from sqlalchemy import (Column, Integer, String, Table, create_engine, MetaData)
from dotenv import load_dotenv
from databases import Database
from datetime import datetime as dt
from pytz import timezone as tz

load_dotenv()
# Database url if none is passed the default one is used
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://hello_fastapi:hello_fastapi@localhost/hello_fastapi_dev")

# SQLAlchemy
engine = create_engine(DATABASE_URL)
metadata = MetaData()
notes = Table(
    "notes",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("title", String(50)),
    Column("description", String(50)),
    Column("completed",String(8), default="False"),
    Column("created_date", String(50), default=dt.now(tz("Africa/Nairobi")).strftime("%Y-%m-%d %H:%M"))
)
# Databases query builder

database = Database(DATABASE_URL)


```

In the code ,we are using [SQLAlchemy](https://www.sqlalchemy.org/) as our ORM(Object Relational Mapper) and [Databases](https://www.encode.io/databases/) as our query builder.

Lets go through what the code does:

* We are importing the required libraries.
* from sqlalchemy we are importing the `Column`, `Integer`, `String`, `Table`, `create_engine` and `MetaData` classes. These are important classes that we will be using to create our database schema and perform CRUD operations.
* We are also using the `dotenv` library to load environment variables from a `.env` file. This is good security practice escpecially in a production environment. When we run the project in a test env, we are providing a default database url. This is the url is important for testing also if the `.env` file is not found.
* We are creating a table called `notes` with the following columns: `id`, `title`, `description`, `completed` and `created_date`. The `id` column is the primary key and the `created_date` column is set to the current date and time.
* We are also creating a database instance using the `Database` class from the `databases` library. This instance will be used to perform CRUD operations.

### Create a CRUD API

![Api-viz](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p5x1f6oysyz023poub1c.jpg)

Inside the `src` folder, create a new folder called `api` and inside it create a new file called `models.py` and add the following code.

```python
# src/api/models.py

from pydantic import BaseModel, Field, NonNegativeInt
from datetime import datetime as dt
from pytz import timezone as tz

class NoteSchema(BaseModel):
    title: str = Field(..., min_length=3, max_length=50) #additional validation for the inputs 
    description: str = Field(...,min_length=3, max_length=50)
    completed: str = "False"
    created_date: str = dt.now(tz("Africa/Nairobi")).strftime("%Y-%m-%d %H:%M")


class NoteDB(NoteSchema):
    id: int 

```

In the code above, we are creating two classes. The `NoteSchema` class is used to validate the data that is sent to the API. The `NoteDB` class is used to validate the data that is returned from the database. The `id` field is added to the `NoteDB` class because it is not sent to the API. It is generated by the database. The `created_date` field is also added to the `NoteDB` class because it is not sent to the API. It is generated by the database.

Here is a list of the fields that we are using:

* `title` - The title of the note. It is a required field and it must be between 3 and 50 characters.
* `description` - The description of the note. It is a required field and it must be between 3 and 50 characters.
* `completed` - The status of the note. It is a required field and it must be either `True` or `False`.
* `created_date` - The date and time when the note was created. It is a required field and it must be in the format `YYYY-MM-DD HH:MM`.

Using Pydantic, we can add additional validation to the fields. For example, we can add a regex to the `title` field to ensure that it only contains letters and numbers. We can also add a regex to the `description` field to ensure that it only contains letters and numbers. We can also add a regex to the `completed` field to ensure that it only contains letters and numbers. We can also add a regex to the `created_date` field to ensure that it only contains letters and numbers.

Inside the `src/api` folder, create a new file called `crud.py` and add the following code.

```python

# src/api/crud.py

from app.api.models import NoteSchema
from app.db import notes, database
from datetime import datetime as dt


async def post(payload: NoteSchema):
    created_date = dt.now().strftime("%Y-%m-%d %H:%M")
    query = notes.insert().values(title=payload.title, 
    description=payload.description, completed=payload.completed, created_date=created_date)
    return await database.execute(query=query)

async def get(id: int):
    query = notes.select().where(id == notes.c.id)
    return await database.fetch_one(query=query)
    

async def get_all():
    query = notes.select()
    return await database.fetch_all(query=query)


async def put(id:int, payload=NoteSchema):
    created_date = dt.now().strftime("%Y-%m-%d %H:%M")
    query = (
        notes.update().where(id == notes.c.id).values(title=payload.title, 
        description=payload.description, completed=payload.completed, created_date=created_date)
        .returning(notes.c.id)
    )
    return await database.execute(query=query)

async def delete(id:int):
    query = notes.delete().where(id == notes.c.id)
    return await database.execute(query=query)
    
```

In the code above, we are creating five functions that will be used to perform CRUD operations. The `post` function is used to create a new note. The `get` function is used to get a note by its id. The `get_all` function is used to get all the notes. The `put` function is used to update a note. The `delete` function is used to delete a note.

Unlike the normal way of defining python functions using the **def** keyword, we are using the **async** keyword to define the functions. This is because we are using a core feature of FastApi which is that its asynchronous. This means that the functions will be executed asynchronously. This is good because it will allow the application to handle multiple requests at the same time.

### Routing and API Endpoints

Inside the `src/api` folder, create a new file called `notes.py` and add the following code.

```python
# src/api/notes.py

from app.api import crud
from app.api.models import NoteDB, NoteSchema
from fastapi import APIRouter, HTTPException, Path
from typing import List 
from datetime import datetime as dt
router = APIRouter()


@router.post("/", response_model=NoteDB, status_code=201)
async def create_note(payload: NoteSchema):
    note_id = await crud.post(payload)
    created_date = dt.now().strftime("%Y-%m-%d %H:%M")

    response_object = {
        "id": note_id,
        "title": payload.title,
        "description": payload.description,
        "completed": payload.completed,
        "created_date": created_date,
    }
    return response_object
    
@router.get("/{id}/", response_model=NoteDB)
async def read_note(id: int = Path(..., gt=0),):
    note = await crud.get(id)
    if not note:
        raise HTTPException(status_code=404, detail="Note not found")
    return note

@router.get("/", response_model=List[NoteDB])
async def read_all_notes():
    return await crud.get_all()

@router.put("/{id}/", response_model=NoteDB)
async def update_note(payload:NoteSchema,id:int=Path(...,gt=0)): #Ensures the input is greater than 0
    note = await crud.get(id)
    if not note:
        raise HTTPException(status_code=404, detail="Note not found")
    note_id = await crud.put(id, payload)
    response_object = {
        "id": note_id,
        "title": payload.title,
        "description": payload.description,
        "completed": payload.completed,
    }
    return response_object

#DELETE route
@router.delete("/{id}/", response_model=NoteDB)
async def delete_note(id:int = Path(...,gt=0)):
    note = await crud.get(id)
    if not note:
        raise HTTPException(status_code=404, detail="Note not found")
    await crud.delete(id)

    return note

```

In the code above, we are creating four functions that will be used to handle the requests. The `create_note` function is used to handle the POST request. The `read_note` function is used to handle the GET request. The `read_all_notes` function is used to handle the GET request. The `update_note` function is used to handle the PUT request. The `delete_note` function is used to handle the DELETE request.

The `create_note` function takes in a payload of type `NoteSchema` and returns a response of type `NoteDB`. The `read_note` function takes in an id of type `int` and returns a response of type `NoteDB`. The `read_all_notes` function returns a response of type `List[NoteDB]`. The `update_note` function takes in a payload of type `NoteSchema` and an id of type `int` and returns a response of type `NoteDB`. The `delete_note` function takes in an id of type `int` and returns a response of type `NoteDB`.

The `@router.post` decorator is used to define the route for the `create_note` function. The `@router.get` decorator is used to define the route for the `read_note` function. The `@router.get` decorator is used to define the route for the `read_all_notes` function. The `@router.put` decorator is used to define the route for the `update_note` function. The `@router.delete` decorator is used to define the route for the `delete_note` function.

### Main File 

Inside the `src` folder, create a new file called `main.py` and add the following code.

```python
# src/main.py

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

from app.api import notes, ping
from app.db import engine, metadata, database

metadata.create_all(engine)

app = FastAPI()

origins = [
    "http://localhost",
    "http://localhost:8080",
    "http://localhost:5173",
    "*"
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["DELETE", "GET", "POST", "PUT"],
    allow_headers=["*"],
)

@app.on_event("startup")
async def startup():
    await database.connect()


@app.on_event("shutdown")
async def shutdown():
    await database.disconnect()

app.include_router(ping.router)
app.include_router(notes.router, prefix="/notes", tags=["notes"])


```

In the code above, we are creating a new FastApi application. We are also adding a middleware to allow cross-origin resource sharing. This is to allow the frontend to make requests to the backend. We are also adding the `notes` and `ping` routers to the application.

On app start up, we are connecting to the database. On app shutdown, we are disconnecting from the database. We are also adding the `notes` and `ping` endpoints  to the application. Ths `notes` endpoint is added to the application with the prefix `/notes`. This means that all the routes in the `notes` endpoint will be prefixed with `/notes`. The `ping` endpoint is added to the application without a prefix. This means that all the routes in the `ping` endpoint will not be prefixed with anything.

## One Database to Rule Them All 👑

In this section, we will be creating a database to store the notes. We will be using [PostgreSQL](https://www.postgresql.org/) as our database. We will be using [Docker](https://www.docker.com/) to run the database. We will be using [Docker Compose](https://docs.docker.com/compose/) to run the database and the application.

### Docker File

Inside the `src` folder, create a new file called `Dockerfile` and add the following code.

```dockerfile
FROM python:3.9.1-alpine

WORKDIR /app

COPY requirements.txt .

RUN pip install -r requirements.txt

COPY . .

CMD ["uvicorn", "src.main:app", "--reload", "--workers", "1", "--host", "0.0.0.0", "--port", "8002"]

```

In the code above, we are creating a new Docker image. We are setting the working directory to `/app`. We are copying the `requirements.txt` file to the working directory. We are installing the dependencies in the `requirements.txt` file. We are copying all the files in the current directory to the working directory. We are setting the command to run when the container is started.

Feel free to change the port number in the command to any port number you want.

### Docker Compose File 📂

Inside the root folder, create a new file called `docker-compose.yml` and add the following code.

```yaml
version: "3.9"

services:
  db:
 image: postgres:13.2
 container_name: postgres
 restart: always
 environment:
  - POSTGRES_USER=hello_fastapi
    - POSTGRES_PASSWORD=hello_fastapi
    - POSTGRES_DB=hello_fastapi_dev
 ports:
   - "5432:5432"
 volumes:
   - ./data:/var/lib/postgresql/data

  app:
 build: .
 container_name: fastapi
 restart: always
 ports:
   - "8002:8002"
 depends_on:
   - db
 volumes:
   - .:/app
 command: uvicorn src.main:app --reload --workers 1 --host


networks:
  default:
      name: hello_fastapi

```

In the code above, we are creating two services. The `db` service is used to run the database. The `app` service is used to run the application. The `db` service is dependent on the `app` service. This means that the `db` service will not start until the `app` service is running.

### Running the Application 🏃


![programmer-viz](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zx4d8vpcllcjgz4kb7r4.jpg)

To run the application locally, run the following command.

```bash
uvicorn src.main:app --reload --workers 1 --host 0.0.0.0 --port 8002
```

The application should be running on port 8002. You can test the application by making requests to the endpoints. You can use [Postman](https://www.postman.com/)/[Insomnia](https://insomnia.rest/) to make requests to the endpoints. You can also use the frontend to make requests to the endpoints.

Screenshot of the application running locally.

![Application Running Locally](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2l0vlmvvyqhndqrxu77.png)


### Testing the Api with Thunder Client 🧪

Personally am using [Visual Studio Code](https://code.visualstudio.com/) as my editor. I have installed the [Thunder Client](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client) extension. This allows me to make requests to the endpoints from within the editor. You can install the extension and make requests to the endpoints from within the editor. Examples as shown below.


![Thunder-Client](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wuchik4tr8b8t69kcr4.png)


![Thunder-Client-get](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ao05ofwdzwt85in3smf8.png)


![Thunder-Client put](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/13j9eqpb5dkidkv8n07x.png)


![Thunder-Client post](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/684vkdfq731ypkdlxg4r.png)


![Thunder-Client delete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zc79fub0aotvj0jtxbak.png)

## CI/CD using GitHub Actions 🐙

In this section, we will be setting up CI/CD using GitHub Actions. We will be using GitHub Actions to test the application. This is important as it alllows to be sure that any proposed changes dont break the api.

We will also implement a workflow to test and build the docker Images for the application and the database. This is important as it allows us to be sure that the application and the database are working as expected.

### Test Workflow 

Inside the `.github/workflows` folder, create a new file called `pythonapp.yml` and add the following code.

```yaml
name: Python Application Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:

  build:
  
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.6, 3.7, 3.8, 3.9]

    steps:
      - uses: actions/checkout@v2
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
            python -m pip install --upgrade pip
            pip install -r requirements.txt
      - name: Lint with flake8
        run: |
            pip install flake8
            # stop the build if there are Python syntax errors or undefined names
            flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
            # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
            flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
      - name: Setup PostgreSQL
        uses: Harmon758/postgresql-action@v1.0.0
        with:
          # Version of PostgreSQL to use
          postgresql version: 12.1-alpine
          # POSTGRES_DB - name for the default database that is created
          postgresql db: hello_fastapi_dev
          # POSTGRES_USER - create the specified user with superuser power
          postgresql user: hello_fastapi
          # POSTGRES_PASSWORD - superuser password
          postgresql password: hello_fastapi
      - name: Test with pytest
        run: |
          pip install pytest
          pytest .
```

In the code above, we are creating a new workflow. We are setting the name of the workflow to `Python Application Test`. We are setting the workflow to run when a push is made to the `main` branch or when a pull request is made to the `main` branch. We are creating a new job called `build`. We are setting the job to run on the latest version of Ubuntu. We are setting the job to run on multiple versions of Python. We are checking out the code. We are setting up Python. We are installing the dependencies. We are linting the code. We are setting up PostgreSQL. We are running the tests.

### Build Workflow

Inside the `.github/workflows` folder, create a new file called `docker-image.yml` and add the following code.

```yaml
# .github/workflows/docker-image.yml

# This is a basic workflow to help you get started with Actions
name: Docker Compose Actions Workflow

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:

  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Build the Docker image
      run: docker-compose build --no-cache --force-rm 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Build the stack
        run: docker-compose up -d

```

In the code above, we are creating a new workflow. We are setting the name of the workflow to `Docker Compose Actions Workflow`. We are setting the workflow to run when a push is made to the `master` branch or when a pull request is made to the `master` branch. We are creating a new job called `build`. We are setting the job to run on the latest version of Ubuntu. We are checking out the code. We are building the docker images for the application and the database.

### Running the Workflows

To run the workflows, push the code to the `main` branch. The workflows will run automatically. You can check the status of the workflows by going to the `Actions` tab on GitHub.

![GitHub Actions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1gv84z39ui2slgakr2fp.png)

## Conclusion

In this article, we have built a simple CRUD API using FastAPI. We have also set up CI/CD using GitHub Actions. We have also set up a PostgreSQL database. We have also set up a docker-compose file to run the application locally.

Find the code for this article on {% embed https://github.com/KenMwaura1/Fast-Api-example %}

Till next time, happy coding.

![Next time gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s7jni1ta016txmdtjr7n.gif)



## References

* [FastAPI](https://fastapi.tiangolo.com/)
* [GitHub Actions](https://docs.github.com/en/actions)
* [PostgreSQL](https://www.postgresql.org/)
* [Docker](https://www.docker.com/)
* [Docker Compose](https://docs.docker.com/compose/)
* [Visual Studio Code](https://code.visualstudio.com/)
* [Thunder Client](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client)
* [SqlAlchemy](https://www.sqlalchemy.org/)


Buy me a [coffee](https://ko-fi.com/kenmwaura1)