A Guide to Starting a FastAPI + Poetry + Serverless Project
Tags: python, webdev, aws, serverless
Published: 2021-10-20 02:28:41

(My first DEV.to article)

Hello, in this article I will go over setting up a basic FastAPI app using Poetry, and deploying/packaging it using Serverless. I will also go over Serverless Lift, and use it to generate a DynamoDB instance.

If you are already aware of the technologies mentioned above, please skip to the next section.

## Section 1 - Introduction to the Technologies.

### [FastAPI](https://fastapi.tiangolo.com/)

FastAPI is a Python-based web framework based on ASGI (Starlette) that is used to make APIs, mostly. As the name suggests, it is much faster than Django and Flask, and comes with a few features (as compared to the star-studded Django) such as pydantic typing, and  OpenAPI documentation. I have written an article on FastAPI over here. 
{% medium https://medium.com/swlh/python-frameworks-and-rest-api-7fa9168b9c67 %}

### [Poetry](https://python-poetry.org/)
 
Poetry is a package manager for Python. For people with background in Javascript, can think of it as a npm manager. Just like `package.json` (`poetry.toml`) and `package-lock.json` (`poetry.lock`), Poetry maintains dependency tree, virtual environments, and also comes with a CLI. 

Using Poetry is not mandatory, I personally am new to it too. Poetry allows us to manage config dependencies and resolve dependency issues which normally occur in old/unmaintained third party libraries that results in conflicted dependencies. Not only that, it allows a better reproduction of the environment and publishing to Pypi.

To install Poetry CLI run `curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -` for osx, linux; and for windows run `(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python -` in Powershell. More instructions [here](https://python-poetry.org/docs/#installation)

### [Serverless](https://www.serverless.com/)

Serverless is gaining a lot of popularity as we are transitioning to a microservices architecture. Serverless comes with a CLI and a dashboard that helps you monitor your serverless functions on a variety of different cloud providers. It provides a higher level layer to monitor and deploy these functions easily. 

I have been using Serverless for a bunch of different side projects and it comes with various plugins to make deployment easier for cloud infrastructures.

To install Serverless CLI, download using `curl -o- -L https://slss.io/install | bash` or `npm install -g serverless` 

## Section 2 -  Starting a FastAPI project with Poetry

After having installed Poetry, let us initialize a poetry project.

```bash
poetry new my-project # change project name to whatever you want
```
This creates a python package with a README, tests directory, and a couple of poetry files. Next we install fastapi using 

```bash
poetry add fastapi uvicorn[standard]
```

These two are required in order to start a FastAPI project and run the ASGI Starlette server. 

In `my-project/my-project` we create our app.py, a pretty basic one for the sake of this project. We write two endpoints for now as follows-

```py
from fastapi import FastAPI
import os

stage = os.environ.get('STAGE', 'dev')


app = FastAPI()

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


@app.get("/users/{user_id}")
def read_item(user_id: int):
    return {"user_id": user_id}
```

To run this project, normally we would run using 
```bash
uvicorn my-project.app:app
```

## Step 3 - Serverless Packaging and Deployment

Now we make a `serverless.yml` in the root folder `my-project`


```yaml
service: My-Project
package:
  individually: true
provider:
  name: aws
  profile: ${opt:aws-profile, "default"}
  region: "us-west-2"
  stage: ${opt:stage, "dev"}
  runtime: python3.8

plugins:
  - serverless-offline
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true
    usePoetry: true

functions:
  app:
    handler: my-project.app.handler #I will explain why we have handler here
    environment:
      STAGE: ${self:provider.stage}
    events:
      - http:
          method: get
          path: /
      - http:
          method: any
          path: /{proxy+}
```

This is pretty easy to understand, we use the standard python packaging plugin for serverless called `serverless-python-requirements`. It packages the python project that contains a `requirements.txt` or a poetry/pipenv package. We point our handler to the `app.py`. 

Right now the app file does not export any handler, so you might be wondering why did I not use `app.app` instead. This is because we have to wrap our ASGI app to adapt AWS Lambda and API Gateway. For that we use [Mangum](https://mangum.io/).

First we install mangum  using 
```
poetry add mangum
```

Next we wrap our app into mangum using the following addition to `app.py`
```python
from mangum import Mangum


handler = Mangum(app)
```

This makes our ASGI app (FastAPI) support API Gateway for HTTP, REST, and WebSockets.


Before proceeding to next step we just have to  initialize our npm project and install  `serverless-python-requirements` to it, since serverless is a node package.

In the project root run 

```bash
npm init --yes
npm install serverless-python-requiremnts
```

## Step 4 - Using Serverless Lift [🎁BONUS🎁]

[Serverless Lift](https://www.serverless.com/plugins/lift) provides another abstraction layer over the AWS SDK to initialize and deploy services like storage, webhooks, static pages, databases, queues and more! We are gonna install it using
```bash
serverless plugin install -n serverless-lift
```

and add it to our `serverless.yml` which should look like
```yaml
plugins:
  - serverless-offline
  - serverless-python-requirements
  - serverless-lift
```

Next, we are gonna construct a DynamoDB table (single table) and use it to access user information in our database.

With the following code in `serverless.yml` we will have the following handled automatically:
⭐ Deploy, if not already exists, a DynamoDB table with generic PK and SK, and up to 20 configurable secondary composite indices called GSIs with generic PKs and SKs for each of them.
⭐ Stream setup with OLD_IMAGES and NEW_IMAGES
⭐ Other small setup configs such as cost set to `pay per req` and TTL enabled.
⭐ Automatically assigned necessary permissions to your lambda functions in this yaml file.
⭐ Variable name injection for table name and stream name

```yaml
constructs:
    myTable:
        type: database/dynamodb-single-table

functions:
  app:
    handler: my-project.app.handler
    environment:
      STAGE: ${self:provider.stage}
      TABLE_NAME: ${construct:myTable.tableName}
    events:
      - http:
          method: get
          path: /
      - http:
          method: any
          path: /{proxy+}
```

Now in our `app.py` we can access this table name and don't have to worry about assigning the lambda an IAM role.

For the users endpoint we can do the following (make sure you populate your DB first obviously, but this is just for the sake of an example): 
```py
@app.get("/users/user_id")
def read_item(user_id: int):
    table_name = os.environ.get('TABLE_NAME', '')
    table = boto3.resource("dynamodb", region_name='us-west-2').Table(table_name)
    response = table.get_item(
        Key={
            'PK': user_id
        }
    )

    return {"user_obj": response['Item']}
```

To make this run just install [boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html)

```
poetry add boto3
```
Et voila!

## Step 5 - Testing and Deploying

To test this locally we run 
```
serverless offline --stage dev --noPrependStageInUrl
```
If you dont include the `--noPrependStageInUrl` flag, it will run your server at `localhost:3000/dev/{proxy}+`. *If you to run it like that, make sure you include `root_path='dev'` parameter in `app=FastAPI()` to see the docs*

We see that it runs locally, and also shows us the docs. To deploy this we use 
```
serverless deploy --stage dev #or prod
```
And serverless will deploy it in our AWS profile, as long as you have the initial serverless config setup. 

### Known issue of serverless-python-requirements is that it will throw a Poetry not found error when you try to deploy or package the sls project. To fix that please go `node_modules/serverless-python-requirements/lib/poetry.js` and replace the `res` at line 17 with 

```js
  const res = spawnSync(
    'poetry',
    [
      'export',
      '--without-hashes',
      '-f',
      'requirements.txt',
      '-o',
      'requirements.txt',
      '--with-credentials',
    ],
    {
      cwd: this.servicePath,
      shell: true // <- we added this 
    }
  );
```
This will prevent that issue. Kudos to this issue creator 
{%github  https://github.com/UnitedIncome/serverless-python-requirements/issues/609 %}


🎉🎉🎉🎉
If you read it till here, thank you for reading the article. Make sure you share and like this article. If you think there is a fault or if I missed something, please reach out.
The code for this is hosted on Github in case anyone is interested.
{% github NimishVerma/ServerlessFastapiPoetry no-readme %}



#### References
1. https://pythonrepo.com/repo/tiangolo-poetry-version-plugin-python-package-dependency-management
2. https://towardsdatascience.com/fastapi-aws-robust-api-part-1-f67ae47390f9
3. https://levelup.gitconnected.com/using-poetry-with-serverless-framework-to-deploy-python-lambda-functions-8dd424727a03