Microservice in Python using FastAPI
Tags: python, fastapi, restapi, microservices
Published: 2020-03-19 11:43:00

# Creating Microservices with Python

<br>

- [Introduction to Microservices](#introduction-microservice)
  - [Benefits of Microservice](#benefits-microservice)
  - [Drawbacks of Microservice](#drawbacks-microservice)
  - [Why Microservice in Python](#why-microservice)
- [Introduction to FastAPI](#introduction-fastapi)
  - [Why FastAPI](#why-fastapi)
  - [Installing FastAPI](#installing-fastapi)
  - [Creating Simple REST API using FastAPI](#creating-simple-rest)
  - [Using PostgreSQL Database with FastAPI](#postgresql-fastapi)
- [Microservice Data Management Patterns](#microservice-data-patterns)
  - [Database Per Service](#database-per-service)
  - [Shared Database](#shared-database)
  - [API Composition](#api-composition)
- [Creating a Python Microservice in Docker](#creating-python-microservice)
  - [Installing Docker and Docker Compose](#installing-docker)
  - [Creating Movies Service](#creating-movie-service)
  - [Creating Casts Service](#creating-casts-service)
  - [Running the microservice using Docker Compose](#running-microservice)
  - [Using Nginx to Access Both Services using a Single Host Address](#using-nginx")
- [Conclusion and Next Step](#conclusion)


As a Python developer you might have heard about the term microservices, and want to build a Python microservice by yourself. Microservices is a great architecture for building highly scalable applications. Before starting to build your application using the microservice, you must be familiar with the benefits and drawbacks of using microservices. In this article, you will learn the benefits and drawbacks of using microservices. You will also learn how you can build your own microservice and deploy it using the Docker Compose.

**In this tutorial you'll learn:**

- What the benefits and drawbacks of microservices are
- Why you should build microservice with Python
- How to build REST API using FastAPI and PostgreSQL
- How to build microservice using FastAPI
- How to run microservices using `docker-compose`
- How to manage microservices using Nginx

You will first build a simple REST API using FastAPI and then use PostgreSQL as our database. You will then extend the same application to a microservice.

## Introduction to Microservices <a id="introduction-microservice"></a>

Microservice is the approach of breaking down large monolith application into individual applications specializing in a specific service/functionality. This approach is often known as **Service-Oriented Architecture** or SOA.

In **monolithic architecture**, every business logic resides in the same application. Application services such as user management, authentication, and other features use the same database.

In a **microservice architecture**, the application is broken down into several separate services that run in separate processes. There is a different database for different functionality of the application and the services communicate with each other using the HTTP, AMQP, or a binary protocol like TCP, depending on the nature of each service. Inter-service communication can also be performed using the message queues like [RabbitMQ](https://www.rabbitmq.com/), [Kafka](https://kafka.apache.org/) or [Redis](https://redis.io/).

### Benefits of Microservice <a id="benefits-microservice"></a>

The microservice architecture comes with lots of benefits. Some of these benefits are:

- Loosely coupled application means the different services can be build using the technologies that suit them best. So, the development team is not bounded to the choices made while starting the project.

- Since the services are responsible for specific functionality which makes it easier to understand and keep the application under control.

- Application scaling also becomes easier because if one of the services requires high GPU usage then only the server consisting that service needs to have high GPU and others can run on a normal server.

### Drawbacks of Microservice <a id="drawbacks-microservice"></a>

The microservice architecture is not a silver bullet that solves all your problems, it comes with its drawbacks too. Some of these drawbacks are:

- Since different services use the different database the transactions involving more than one service needs to use eventual consistency.

- Perfect splitting of the services is very difficult to achieve at the first try and this needs to be iterated before coming with the best possible separation of the services.

- Since the services communicate with each other through the use of network interaction, this makes the application slower due to the network latency and slow service.

### Why Microservice in Python <a id="why-microservice"></a>

Python is a perfect tool for building micro-services because it comes with a great community, easy learning curve and tons of libraries. Due to the introduction of asynchronous programming in Python, [web frameworks](https://fastapi.tiangolo.com/features/#starlette-features) with performance on-par with GO and Node.js, has emerged.

## Introduction to FastAPI <a id="introduction-fastapi"></a>

FastAPI is a modern, high-performance, web framework, which comes with tons of cool features like auto-documentation based on OpenAPI and built-in serialization and validation library. See [here](https://fastapi.tiangolo.com/features/) for the list of all cool features in FastAPI.

### Why FastAPI <a id="why-fastapi"></a>

Some of the reason why I think FastAPI is a great choice for building microservices in Python is:

- Auto documentation
- Async/Await support
- Built-in validation and serialization
- 100% type annotated so autocompletion works great

### Installing FastAPI <a id="installing-fastapi"></a>

Before installing FastAPI create a new directory `movie_service` and create a new virtual environment inside the newly created directory using [virtualenv](https://realpython.com/python-virtual-environments-a-primer/).
If you haven't already installed `virtualenv`:

```console
pip install virtualenv
```

Now, create a new virtual environment.

```console
virtualenv env
```

If you are on Mac/Linux you can activate the virtual environment using the command:

```console
source ./env/bin/activate
```

Windows users can run this command instead:

```doscon
.\env\Scripts\activate
```

Finally, Your are ready to install FastAPI, run the following command:

```console
pip install fastapi
```

Since FastAPI doesn't come with inbuilt service, you need to install `uvicorn` for it to run. `uvicorn` is an [ASGI](https://florimond.dev/blog/articles/2019/08/introduction-to-asgi-async-python-web/) server which allows us to use async/await features.
Install `uvicorn` using the command

```console
pip install uvicorn
```

### Creating Simple REST API using FastAPI <a id="creating-simple-rest"></a>

Before You start building a microservice using FastAPI, let's learn the basics of FastAPI. Create a new directory `app` and a new file `main.py` inside the newly-created directory.

Add the following code in `main.py`.

```python
#~/movie_service/app/main.py

from fastapi import FastAPI

app = FastAPI()


@app.get('/')
async def index():
    return {"Real": "Python"}
```

Here you first import and instantiate the FastAPI and then register the root endpoint `/` which then returns a `JSON`.

You can run the application server using `uvicorn app.main:app --reload`. Here `app.main` indicates you use `main.py` file inside the `app` directory and `:app` indicates our `FastAPI` instance name.

You can access the app from http://127.0.0.1:8000. To access the cool automatic documentation, head over to http://127.0.0.1:8000/docs. You can play around and interact with your API from the browser itself.

Let's add some CRUD functionality to our application.
Update your `main.py` to look like the following:

```python
#~/movie_service/app/main.py

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

app = FastAPI()

fake_movie_db = [
    {
        'name': 'Star Wars: Episode IX - The Rise of Skywalker',
        'plot': 'The surviving members of the resistance face the First Order once again.',
        'genres': ['Action', 'Adventure', 'Fantasy'],
        'casts': ['Daisy Ridley', 'Adam Driver']
    }
]

class Movie(BaseModel):
    name: str
    plot: str
    genres: List[str]
    casts: List[str]


@app.get('/', response_model=List[Movie])
async def index():
    return fake_movie_db
```

As you can see you have created a new class `Movie` which extends `BaseModel` from pydantic.
The `Movie` model contains the name, phot, genres, and casts. [Pydantic](https://pydantic-docs.helpmanual.io/) comes built-in with FastAPI which makes making models and request validation a breeze.

If you head over to the docs site you can see there are fields of our Movies model mentioned already in the example response section. This is possible because you have defined the `response_model` in our route definition.

Now, let's add the endpoint to add a movie to our movies list.

Add a new endpoint definition to handle the `POST` request.

```python
@app.post('/', status_code=201)
async def add_movie(payload: Movie):
    movie = payload.dict()
    fake_movie_db.append(movie)
    return {'id': len(fake_movie_db) - 1}
```

Now, head over to the browser and test the new API. Try adding a movie with an invalid field or without the required fields and see that the validation is automatically handled by FastAPI.

Let's add a new endpoint to update the movie.

```python
@app.put('/{id}')
async def update_movie(id: int, payload: Movie):
    movie = payload.dict()
    movies_length = len(fake_movie_db)
    if 0 <= id <= movies_length:
        fake_movie_db[id] = movie
        return None
    raise HTTPException(status_code=404, detail="Movie with given id not found")
```

Here `id` is the index of our `fake_movie_db` list. <br>

**Note: Remember to import `HTTPException` from `fastapi`**

Now you can also add the endpoint to delete the movie.

```python
@app.delete('/{id}')
async def delete_movie(id: int):
    movies_length = len(fake_movie_db)
    if 0 <= id <= movies_length:
        del fake_movie_db[id]
        return None
    raise HTTPException(status_code=404, detail="Movie with given id not found")
```

Before you move forward, let's structure our app in a better way. Create a new folder `api` inside `app` and create a new file `movies.py` inside the recently created folder. Move all the routes related codes from `main.py` to `movies.py`. So, the `movies.py` should look like the following:

```python
#~/movie-service/app/api/movies.py

from typing import List
from fastapi import Header, APIRouter

from app.api.models import Movie

fake_movie_db = [
    {
        'name': 'Star Wars: Episode IX - The Rise of Skywalker',
        'plot': 'The surviving members of the resistance face the First Order once again.',
        'genres': ['Action', 'Adventure', 'Fantasy'],
        'casts': ['Daisy Ridley', 'Adam Driver']
    }
]

movies = APIRouter()

@movies.get('/', response_model=List[Movie])
async def index():
    return fake_movie_db

@movies.post('/', status_code=201)
async def add_movie(payload: Movie):
    movie = payload.dict()
    fake_movie_db.append(movie)
    return {'id': len(fake_movie_db) - 1}

@movies.put('/{id}')
async def update_movie(id: int, payload: Movie):
    movie = payload.dict()
    movies_length = len(fake_movie_db)
    if 0 <= id <= movies_length:
        fake_movie_db[id] = movie
        return None
    raise HTTPException(status_code=404, detail="Movie with given id not found")

@movies.delete('/{id}')
async def delete_movie(id: int):
    movies_length = len(fake_movie_db)
    if 0 <= id <= movies_length:
        del fake_movie_db[id]
        return None
    raise HTTPException(status_code=404, detail="Movie with given id not found")
```

Here you registered a new API route using the [APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/#apirouter) from FastAPI.

Also, create a new file `models.py` inside `api` where you will be keeping our Pydantic models.

```python
#~/movie-service/api/models.py

from typing import List
from pydantic import BaseModel

class Movie(BaseModel):
    name: str
    plot: str
    genres: List[str]
    casts: List[str]

```

Now register this new routes file in `main.py`

```python
#~/movie-service/app/main.py
from fastapi import FastAPI

from app.api.movies import movies

app = FastAPI()

app.include_router(movies)
```

Finally, our application directory structure looks like this:

```
movie-service
├── app
│   ├── api
│   │   ├── models.py
│   │   ├── movies.py
│   |── main.py
└── env
```

Make sure your application is working properly before you move forward.

### Using PostgreSQL Database with FastAPI <a id="postgresql-fastapi"></a>

Previously, you used fake Python list to add movies but now you are finally ready to use an actual database for this purpose. You are going to use [PostgreSQL](https://www.postgresql.org/) for this purpose. [Install PostgreSQL](https://www.postgresql.org/download/) if you haven't already. After installing the PostgreSQl create a new database, I am going to call mine `movie_db`.

You are going to use [encode/databases](https://github.com/encode/databases) to connect to the database using `async` and `await` support. Learn more about `async/await` in Python [here](https://florimond.dev/blog/articles/2019/08/introduction-to-asgi-async-python-web/)

Install the required library using:

```console
pip install 'databases[postgresql]'
```

this will install `sqlalchemy` and `asyncpg` as well, which are required for working with PostgreSQL.

Create a new file inside `api` and call it `db.py`. This file will contain the actual database model for our REST API.

```python
#~/movie-service/app/api/db.py

from sqlalchemy import (Column, Integer, MetaData, String, Table,
                        create_engine, ARRAY)

from databases import Database

DATABASE_URL = 'postgresql://movie_user:movie_password@localhost/movie_db'

engine = create_engine(DATABASE_URL)
metadata = MetaData()

movies = Table(
    'movies',
    metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(50)),
    Column('plot', String(250)),
    Column('genres', ARRAY(String)),
    Column('casts', ARRAY(String))
)

database = Database(DATABASE_URL)
```

Here, `DATABASE_URI` is the URL used to connect to the PostgreSQL database. Here `movie_user` is the name of the database user, `movie_password` is the password of the database user and `movie_db` is the name of the database.

Just like you would to in [SQLAlchemy](https://realpython.com/flask-by-example-part-2-postgres-sqlalchemy-and-alembic/) you have created the table for the movies database.

Update `main.py` to connect to the database. `main.py` should look like the following:

```python
#~/movie-service/app/main.py

from fastapi import FastAPI
from app.api.movies import movies
from app.api.db import metadata, database, engine

metadata.create_all(engine)

app = FastAPI()

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

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


app.include_router(movies)
```

FastAPI provides some event handlers which you can use to connect to our database when the application starts and disconnect when it shuts down.

Update `movies.py` so that it uses a database instead of a fake Python list.

```python
#~/movie-service/app/api/movies.py


from typing import List
from fastapi import Header, APIRouter

from app.api.models import MovieIn, MovieOut
from app.api import db_manager

movies = APIRouter()

@movies.get('/', response_model=List[MovieOut])
async def index():
    return await db_manager.get_all_movies()

@movies.post('/', status_code=201)
async def add_movie(payload: MovieIn):
    movie_id = await db_manager.add_movie(payload)
    response = {
        'id': movie_id,
        **payload.dict()
    }

    return response

@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
    movie = payload.dict()
    fake_movie_db[id] = movie
    return None

@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
    movie = await db_manager.get_movie(id)
    if not movie:
        raise HTTPException(status_code=404, detail="Movie not found")

    update_data = payload.dict(exclude_unset=True)
    movie_in_db = MovieIn(**movie)

    updated_movie = movie_in_db.copy(update=update_data)

    return await db_manager.update_movie(id, updated_movie)

@movies.delete('/{id}')
async def delete_movie(id: int):
    movie = await db_manager.get_movie(id)
    if not movie:
        raise HTTPException(status_code=404, detail="Movie not found")
    return await db_manager.delete_movie(id)
```

Let's add `db_manager.py` to manipulate our database.

```python
#~/movie-service/app/api/db_manager.py

from app.api.models import MovieIn, MovieOut, MovieUpdate
from app.api.db import movies, database


async def add_movie(payload: MovieIn):
    query = movies.insert().values(**payload.dict())

    return await database.execute(query=query)

async def get_all_movies():
    query = movies.select()
    return await database.fetch_all(query=query)

async def get_movie(id):
    query = movies.select(movies.c.id==id)
    return await database.fetch_one(query=query)

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

async def update_movie(id: int, payload: MovieIn):
    query = (
        movies
        .update()
        .where(movies.c.id == id)
        .values(**payload.dict())
    )
    return await database.execute(query=query)
```

Let's update our `models.py` so that you can use the Pydantic model with the sqlalchemy table.

```python
#~/movie-service/app/api/models.py

from pydantic import BaseModel
from typing import List, Optional

class MovieIn(BaseModel):
    name: str
    plot: str
    genres: List[str]
    casts: List[str]


class MovieOut(MovieIn):
    id: int


class MovieUpdate(MovieIn):
    name: Optional[str] = None
    plot: Optional[str] = None
    genres: Optional[List[str]] = None
    casts: Optional[List[str]] = None
```

Here `MovieIn` is the base model that you use to add the movie to the database. You have to add the `id` to this model while getting it from the database, hence the `MovieOut` model. `MovieUpdate` model allows us to set the values in the model to be optional so that while updating the movie only the field that needs to be updated can be sent.

Now, head over to the browser documentation site and start playing with the API.

## Microservice Data Management Patterns <a id="microservice-data-patterns"></a>

Managing data in microservice is one of the most challenging aspects of building a microservice. Since different functions of the application are handled by different services, usage of a database can be tricky.

Here are some patterns that you can use to manage data flow in the application.

### Database Per Service <a id="database-per-service"></a>

Using a database per service is great if you want your microservices to be as loosely coupled as possible. Having a different database per service allows us to scale different services independently. A transaction involving multiple databases is done through well-defined APIs. This comes with its drawback as implementing business transactions involving multiple services is not straightforward. Also, the addition of network overhead makes this less efficient to use.

### Shared Database <a id="shared-database"></a>

If there are lots of transactions involving multiple services it is better to use a shared database. This comes with the benefits of highly consistent application but takes away most of the benefits that come with the microservices architecture. Developers working on one service needs to coordinate with the schema changes in other services.

### API Composition <a id="api-composition"></a>

In transactions involving multiple databases, API composer acts as an API gateway and executes API calls to other microservices in the required order. Finally, the results from each microservices are returned to the client service after performing an in-memory join. The downside of this approach is inefficient in-memory joins of a large dataset.

## Creating a Python Microservice in Docker <a id="creating-python-microservice"></a>

The pain of deploying the microservice can be greatly reduced by using the Docker. Docker helps to encapsulate each service and scale them independently.

### Installing Docker and Docker Compose <a id="installing-docker"></a>

If you haven't already [install docker](https://docs.docker.com/install/) in your system. Verify if the docker is installed by running the command `docker`. After you have done installing Docker, [install Docker Compose](https://docs.docker.com/compose/install/). Docker Compose is used for defining and running multiple Docker containers. It also helps in easy interaction between them.

### Creating Movies Service <a id="creating-movie-service"></a>

Since a lot of the work for building a movie service is already done while getting started with the FastAPI, you are going to reuse the code you have already written. Create a brand new folder, I am going to call mine `python-microservices`. Move the code you wrote earlier which I had named `movie-service`.
So, the folder structure would look like this:

```
python-microservices/
└── movie-service/
    ├── app/
    └── env/
```

First of all, let's create a `requirements.txt` file where you are going to keep all the dependencies you are going to use in our `movie-service`.
Create a new file `requirements.txt` inside `movie-service` and add the following to it:

```
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
httpx==0.11.1
```

You have used all the libraries mentioned there except [httpx](https://github.com/encode/httpx/) which you are going to use while making service to service API call.

Create a `Dockerfile` inside `movie-service` with the following contents:

```docker
FROM python:3.8-slim

WORKDIR /app

COPY ./requirements.txt /app/requirements.txt

RUN apt-get update \
    && apt-get install gcc -y \
    && apt-get clean

RUN pip install -r /app/requirements.txt \
    && rm -rf /root/.cache/pip

COPY . /app/
```

Here first, you define which Python version you want to use. Then set the `WORKDIR` to be inside `app` folder inside the Docker container. After that `gcc` is installed which is required by the libraries that you are using in the application.
Finally, install all dependencies in `requirements.txt` and copy all the files inside `movie-service/app`.

Update `db.py` and replace

```python
DATABASE_URI = 'postgresql://movie_user:movie_password@localhost/movie_db'
```

with

```python
DATABASE_URI = os.getenv('DATABASE_URI')
```

**Note: Don't forget to import `os` on the top of the file.**

You need to do this so that you can latter provide `DATABASE_URI` as an environment variable.

Also, update `main.py` and replace

```python
app.include_router(movies)
```

with

```python
app.include_router(movies, prefix='/api/v1/movies', tags=['movies'])
```

Here, you have added `prefix` `/api/v1/movies` so, that managing different version of API becomes easier. Also, tags make finding API related to `movies` easier in FastAPI docs.

Also, you need to update our models so that the `casts` stores the cast's id instead of the actual name. So, update the `models.py` to look like this:

```python
#~/python-microservices/movie-service/app/api/models.py

from pydantic import BaseModel
from typing import List, Optional

class MovieIn(BaseModel):
    name: str
    plot: str
    genres: List[str]
    casts_id: List[int]


class MovieOut(MovieIn):
    id: int


class MovieUpdate(MovieIn):
    name: Optional[str] = None
    plot: Optional[str] = None
    genres: Optional[List[str]] = None
    casts_id: Optional[List[int]] = None
```

Likewise, you need to update the database tables, let's update `db.py`:

```python
#~/python-microservices/movie-service/app/api/db.py

import os

from sqlalchemy import (Column, DateTime, Integer, MetaData, String, Table,
                        create_engine, ARRAY)

from databases import Database

DATABASE_URL = os.getenv('DATABASE_URL')

engine = create_engine(DATABASE_URL)
metadata = MetaData()

movies = Table(
    'movies',
    metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(50)),
    Column('plot', String(250)),
    Column('genres', ARRAY(String)),
    Column('casts_id', ARRAY(Integer))
)

database = Database(DATABASE_URL)
```

Now, update `movies.py` to check if the cast with the given id presents in cast service before adding a new movie or updating a movie.

```python
#~/python-microservices/movie-service/app/api/movies.py

from typing import List
from fastapi import APIRouter, HTTPException

from app.api.models import MovieOut, MovieIn, MovieUpdate
from app.api import db_manager
from app.api.service import is_cast_present

movies = APIRouter()

@movies.post('/', response_model=MovieOut, status_code=201)
async def create_movie(payload: MovieIn):
    for cast_id in payload.casts_id:
        if not is_cast_present(cast_id):
            raise HTTPException(status_code=404, detail=f"Cast with id:{cast_id} not found")

    movie_id = await db_manager.add_movie(payload)
    response = {
        'id': movie_id,
        **payload.dict()
    }

    return response

@movies.get('/', response_model=List[MovieOut])
async def get_movies():
    return await db_manager.get_all_movies()

@movies.get('/{id}/', response_model=MovieOut)
async def get_movie(id: int):
    movie = await db_manager.get_movie(id)
    if not movie:
        raise HTTPException(status_code=404, detail="Movie not found")
    return movie

@movies.put('/{id}/', response_model=MovieOut)
async def update_movie(id: int, payload: MovieUpdate):
    movie = await db_manager.get_movie(id)
    if not movie:
        raise HTTPException(status_code=404, detail="Movie not found")

    update_data = payload.dict(exclude_unset=True)

    if 'casts_id' in update_data:
        for cast_id in payload.casts_id:
            if not is_cast_present(cast_id):
                raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found")

    movie_in_db = MovieIn(**movie)

    updated_movie = movie_in_db.copy(update=update_data)

    return await db_manager.update_movie(id, updated_movie)

@movies.delete('/{id}', response_model=None)
async def delete_movie(id: int):
    movie = await db_manager.get_movie(id)
    if not movie:
        raise HTTPException(status_code=404, detail="Movie not found")
    return await db_manager.delete_movie(id)
```

Let's add a service to make an API call to cast service:

```python
#~/python-microservices/movie-service/app/api/service.py

import os
import httpx

CAST_SERVICE_HOST_URL = 'http://localhost:8002/api/v1/casts/'
url = os.environ.get('CAST_SERVICE_HOST_URL') or CAST_SERVICE_HOST_URL

def is_cast_present(cast_id: int):
    r = httpx.get(f'{url}{cast_id}')
    return True if r.status_code == 200 else False

```

You make an api call to get the cast with the given id and return true if the cast exists and false otherwise.

### Creating Casts Service <a id="creating-casts-service"></a>

Similar to a `movie-service`, for creating a `casts-service` you are going to use FastAPI and PostgreSQL database.

Create a folder structure like the following:

```
python-microservices/
.
├── cast_service/
│   ├── app/
│   │   ├── api/
│   │   │   ├── casts.py
│   │   │   ├── db_manager.py
│   │   │   ├── db.py
│   │   │   ├── models.py
│   │   ├── main.py
│   ├── Dockerfile
│   └── requirements.txt
├── movie_service/
...
```

Add the following to `requirements.txt`:

```
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
```

`Dockerfile`:

```docker
FROM python:3.8-slim

WORKDIR /app

COPY ./requirements.txt /app/requirements.txt

RUN apt-get update \
    && apt-get install gcc -y \
    && apt-get clean

RUN pip install -r /app/requirements.txt \
    && rm -rf /root/.cache/pip

COPY . /app/
```

`main.py`

```python
#~/python-microservices/cast-service/app/main.py

from fastapi import FastAPI
from app.api.casts import casts
from app.api.db import metadata, database, engine

metadata.create_all(engine)

app = FastAPI()

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

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

app.include_router(casts, prefix='/api/v1/casts', tags=['casts'])
```

You have added the prefix of `/api/v1/casts` so that managing the API becomes easier. Also, adding `tags` makes finding docs related to `casts` in the FastAPI docs easier.

`casts.py`

```python
#~/python-microservices/cast-service/app/api/casts.py

from fastapi import APIRouter, HTTPException
from typing import List

from app.api.models import CastOut, CastIn, CastUpdate
from app.api import db_manager

casts = APIRouter()

@casts.post('/', response_model=CastOut, status_code=201)
async def create_cast(payload: CastIn):
    cast_id = await db_manager.add_cast(payload)

    response = {
        'id': cast_id,
        **payload.dict()
    }

    return response

@casts.get('/{id}/', response_model=CastOut)
async def get_cast(id: int):
    cast = await db_manager.get_cast(id)
    if not cast:
        raise HTTPException(status_code=404, detail="Cast not found")
    return cast
```

`db_manager.py`

```python
#~/python-microservices/cast-service/app/api/db_manager.py

from app.api.models import CastIn, CastOut, CastUpdate
from app.api.db import casts, database


async def add_cast(payload: CastIn):
    query = casts.insert().values(**payload.dict())

    return await database.execute(query=query)

async def get_cast(id):
    query = casts.select(casts.c.id==id)
    return await database.fetch_one(query=query)
```

`db.py`

```python
#~/python-microservices/cast-service/app/api/db.py

import os

from sqlalchemy import (Column, Integer, MetaData, String, Table,
                        create_engine, ARRAY)

from databases import Database

DATABASE_URI = os.getenv('DATABASE_URI')

engine = create_engine(DATABASE_URI)
metadata = MetaData()

casts = Table(
    'casts',
    metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(50)),
    Column('nationality', String(20)),
)

database = Database(DATABASE_URI)
```

`models.py`

```python
#~/python-microservices/cast-service/app/api/models.py

from pydantic import BaseModel
from typing import List, Optional

class CastIn(BaseModel):
    name: str
    nationality: Optional[str] = None


class CastOut(CastIn):
    id: int


class CastUpdate(CastIn):
    name: Optional[str] = None
```

### Running the microservice using Docker Compose <a id="running-microservice"></a>

To run the microservices, create a `docker-compose.yml` file and add the following to it:

```
version: '3.7'

services:
  movie_service:
    build: ./movie-service
    command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
    volumes:
      - ./movie-service/:/app/
    ports:
      - 8001:8000
    environment:
      - DATABASE_URI=postgresql://movie_db_username:movie_db_password@movie_db/movie_db_dev
      - CAST_SERVICE_HOST_URL=http://cast_service:8000/api/v1/casts/

  movie_db:
    image: postgres:12.1-alpine
    volumes:
      - postgres_data_movie:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=movie_db_username
      - POSTGRES_PASSWORD=movie_db_password
      - POSTGRES_DB=movie_db_dev

  cast_service:
    build: ./cast-service
    command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
    volumes:
      - ./cast-service/:/app/
    ports:
      - 8002:8000
    environment:
      - DATABASE_URI=postgresql://cast_db_username:cast_db_password@cast_db/cast_db_dev

  cast_db:
    image: postgres:12.1-alpine
    volumes:
      - postgres_data_cast:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=cast_db_username
      - POSTGRES_PASSWORD=cast_db_password
      - POSTGRES_DB=cast_db_dev

volumes:
  postgres_data_movie:
  postgres_data_cast:
```

Here you have 4 different services, movie_service, a database for movie_service, cast_service and a database for cast service. You have exposed `movie_service` to port `8001` similarly `cast_service` to port `8002`.

For the database, you have used volumes so that the data doesn't get destroyed when the docker container is shutdown.

Run the docker-compose using the command:

```console
docker-compose up -d
```

This creates the docker image if it doesn't already exist and runs them.

Head over to http://localhost:8002/docs to add a cast in casts service. Similarly, http://localhost:8001/docs to add the movie in the movie service.

### Using Nginx to Access Both Services using a Single Host Address <a id="using-nginx"></a>

You have deployed the microservices using Docker compose but there is one minor problem. Each of the microservices needs to be accessed using a different Port. You can solve this issue using [Nginx reverse proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/), using Nginx you can direct the request add a middleware which routes our requests to different services based on the API URL.

Add a new file `nginx_config.conf` inside `python-microservices` with the following contents.

```conf

server {
  listen 8080;

  location /api/v1/movies {
    proxy_pass http://movie_service:8000/api/v1/movies;
  }

  location /api/v1/casts {
    proxy_pass http://cast_service:8000/api/v1/casts;
  }

}
```

Here you are running the Nginx at port `8080` and routing the requests to movie service if the endpoint starts with `/api/v1/movies` and similarly to cast service if the endpoint starts with `/api/v1/casts`

Now, you need to add the nginx service in our `docker-compose-yml`. Add the following service after `cast_db` service:

```docker
nginx:
    image: nginx:latest
    ports:
      - "8080:8080"
    volumes:
      - ./nginx_config.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - cast_service
      - movie_service
```

Now, shut down the containers with the command:

```console
docker-compose down
```

And run it back again with:

```console
docker-compose up -d
```

Now, you can access both movie service and cast service at port `8080`.
Head over to http://localhost:8080/api/v1/movies/ to get the list of movies.

Now, you might be wondering how you can access the docs of the services. For that update `main.py` of movie service and replace

```python
app = FastAPI()
```

with

```python
app = FastAPI(openapi_url="/api/v1/movies/openapi.json", docs_url="/api/v1/movies/docs")
```

Similarly, for cast service replace it with

```python
app = FastAPI(openapi_url="/api/v1/casts/openapi.json", docs_url="/api/v1/casts/docs")
```

Here, you changed which endpoint the docs are served and from where the `openapi.json` is served.

Now, you can access the docs from http://localhost:8080/api/v1/movies/docs for movie service and from http://localhost:8080/api/v1/casts/docs for casts service.

**If you are stuck at some point or just want to have a look at the complete code, head over to the [Github Repo](https://github.com/paurakhsharma/python-microservice-fastapi)**

## Conclusion and Next Step <a id="conclusion"></a>

The microservice architecture is great for breaking down a large monolith application into separate business logics but this comes with the complication too. Python is great for building microservice because of the developer experience and tons of packages and frameworks to make developers more productive.

Deploying microservices has become easier thanks to Docker. Learn more on [How to develop microservices using Docker, and Docker Compose](https://aws.amazon.com/blogs/publicsector/how-to-develop-microservices-using-aws-cloud9-docker-and-docker-compose/)


**Want me to cover any topic? Let me know at [twitter](https://twitter.com/paurakhsharma) or write a comment down below.**