Contents

Building And Deploying Rock Paper Scissors With Python FastAPI And Deta

See the code for this project on GitHub.


In case you have haven’t heard, FastAPI is Python’s new web framework alternative to Django and flask. It prides itself on being fast - both in terms of performance and codeability. (And no, “codeability” is not a word but it should be.)

Background

I want to a build a SaaS. I have an idea, but I need to build a prototype. Enter FastAPI. I know the basics of web app development, but I’m not a web app developer. I’m a data scientist. I’ve spent the last two days reading up on FastAPI, attempting to understand how it works. So far, so good, but I’m realizing the monumental effort it’s going to take to get even a simple production-ready SaaS up and running. The hard part is not FastAPI. The hard part is deploying FastAPI to a server, standing up and connecting to a database, handling user registration and authentication, payment processing, plan tiers, upgrades and downgrades, version control, backend data administration, learning React? Docker? Google Cloud Run? Firebase? Deta? What the F am I doing? Help me oh God Jesus.

Gripe

The key to learning something difficult is creating and retaining the motivation you need to dredge through the boring but necessary stuff. Unfortunately, most existing courses/tutorials about FastAPI stand over your body, beating the motivation out of you with a thousand wooden bats called “cookies”, “middleware”, and “testing”. Theoretically, there’s someone out there who knows every nook and cranny of FastAPI, who’s never actually deployed something for the world to see and use.

I for one want to build and deploy something - something simple - but I want to put something out there for the world to see - well probably just me and my wife - actually, probably just me. But nonetheless, I want to deploy something that theoretically could be viewed by other people. Not only because deployment has been this giant grey question mark in my head as I’ve been reading through the FastAPI docs but also because dangling the carrot of actually deploying something that is public and works is going to keep me motivated. And when I’m motivated, my brain tends to process the words in the docs better.

Rock Paper Scissors

What to build? It needs to be challenging enough to be interesting, but simple enough to be doable given my current knowledge. For me, that’s the game Rock Paper Scissors. Let’s begin.

Setup

First of all, I’m on a mac, although I don’t think it matters much.

Virtual Environment

I’m going to start by creating a virtual environment; that way I’ll have a fresh and isolated installation of the packages I’ll be using for my app. Most people I’ve seen use python’s built in venv function, but I’m more familiar with conda, so that’s what I’ll use. For me, creating a new virtual environment with conda looks like this

(run in terminal)

conda create -n rockenv python=3.9

Here I’ve created a new virtual environment called rockenv with python 3.9 installed. Next, I need to install FastAPI on this environment which I can do by

  1. activating rockenv with conda activate rockenv
  2. installing fastapi with pip install 'fastapi[all]'

Your experience may differ. See the docs for installation details.

Project Structure

Next, I need to set up an empty folder on my mac which I’ll aptly name rock-paper-scissors/. This’ll be the root directory of my project.

Since I’m using PyCharm as my IDE, I’ll create a new PyCharm project attached to my rock-paper-scissors/ directory and set my project interpreter as rockenv (the virtual environment I created above ^^).

Now I’ll create a file called main.py within rock-paper-scissors/ (so, rock-paper-scissors/main.py), shamelessly copying and pasting the starter code from the FastAPI docs.

main.py

from fastapi import FastAPI

app = FastAPI()


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

To see that it works, I’ll run the app locally with uvicorn main:app --reload.

Then I’ll navigate the to http://127.0.0.1:8000 - uvicorn’s development URL - and observe the JSON response {"message":"Hello World"}.

Awesome!

Deployment

At this point, I want to focus on deployment. My app is boring, but I can improve it later. After tinkering with deployment options for a while, I’ve found Deta.sh to be the easiest option. The FastAPI docs give a great explanation of deployment with Deta, but I’ll walk you through the process myself.

1. Add requirements.txt

First we create a requirements.txt file inside rock-paper-scissors/ with one line per required package (and optionally version details). This tells Deta what packages to install to run your app. For me, requirements.txt is simply a one-line file like this

fastapi==0.68.1

2. Create a free Deta account

You can register here. After registering, you’ll need to confirm your email address by clicking the link inside the auto-generated email sent to you.

3. Install the Deta CLI

Next, install the Deta Command Line Interface on your computer by opening Terminal and running

curl -fsSL https://get.deta.dev/cli.sh | sh

You may need to close Terminal and reopen it for the Deta CLI to be recognized. Run deta --help to see that it’s working.

4. Login with the Deta CLI

From Terminal, run deta login and log in to your Deta account.

5. Deploy your app with the Deta CLI

cd into the root of our project (rock-paper-scissors/) and then run deta new. This will deploy the application onto deta in a development state, only accessible by you, assuming you’re logged in to your Deta account. (Tip: You can quickly log in to Deta from Terminal with deta login).

To make your app public to the world, run deta auth disable.

6. Check it out

Upon running deta new, you should get a response with some details about your app like its name, id, endpoint, etc. My endpoint, for example, is https://02rkyn.deta.dev/. When I go there, I see the JSON response {"message":"Hello World"} just as I did in my development environment.

Sweet.

Implementing Rock Paper Scissors

Now let’s implement rock paper scissors. A sensible interface for this would be to set up an endpoint like /shoot/<weapon>. For example, a user might hit /shoot/rock or /shoot/paper. And then we should return something like

{
  user_weapon: rock,
  opponent_weapon: scissors,
  message: 'You won!'
}

Here’s a first pass at implementing the logic for this endpoint.

from fastapi import FastAPI
from random import randrange

app = FastAPI()

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

@app.get('/shoot/{weapon}')
async def shoot(weapon: str):

    game_key = {
        ('rock', 'rock'): "It's a tie.",
        ('rock', 'paper'): "You lost.",
        ('rock', 'scissors'): "You won!",
        ('paper', 'rock'): "You won!",
        ('paper', 'paper'): "It's a tie.",
        ('paper', 'scissors'): "You lost.",
        ('scissors', 'rock'): "You lost.",
        ('scissors', 'paper'): "You won!",
        ('scissors', 'scissors'): "It's a tie.",
    }
    weapons = ['rock', 'paper', 'scissors']
    opp_weapon = weapons[randrange(0, 3)]
    message = game_key[(weapon, opp_weapon)]
    result = {'user_weapon': weapon, 'opponent_weapon': opp_weapon, 'message': message}

    return result
  1. I use the @app.get() decorator as this is a GET method because we’re not inserting, updating, or deleting anything from a database.
  2. I use a path parameter called weapon which feeds into my function shoot(weapon: str):
  3. weapon: str is a type annotation that basically says “the inputted weapon should be a string”
  4. I use the random module to randomly pick the opponent’s weapon
  5. I combine the user’s weapon and the opponent’s weapon into a tuple (weapon, opp_weapon) to form a key. Then I use that key to lookup the appropriate message to display from my dictionary of all possible weapon combinations.

This works, but with a glaring issue. What if the user inputs garbage like /shoot/bazooka?

In this case we get “Internal Server Error” because the game_key[(weapon, opp_weapon)] is looking for a key that doesn’t exist. To prevent this, we’ll create an Enum restricted to the strings ‘rock’, ‘paper’, and ‘scissors’, and then we’ll set the weapon parameter’s type annotation to that enum.

from fastapi import FastAPI
from random import randrange
from enum import Enum

app = FastAPI()

class Weapon(str, Enum):
    rock = 'rock'
    paper = 'paper'
    scissors = 'scissors'

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

@app.get('/shoot/{weapon}')
async def shoot(weapon: Weapon):

    game_key = {
        ('rock', 'rock'): "It's a tie.",
        ('rock', 'paper'): "You lost.",
        ('rock', 'scissors'): "You won!",
        ('paper', 'rock'): "You won!",
        ('paper', 'paper'): "It's a tie.",
        ('paper', 'scissors'): "You lost.",
        ('scissors', 'rock'): "You lost.",
        ('scissors', 'paper'): "You won!",
        ('scissors', 'scissors'): "It's a tie.",
    }
    weapons = ['rock', 'paper', 'scissors']
    opp_weapon = weapons[randrange(0, 3)]
    message = game_key[(weapon, opp_weapon)]
    result = {'user_weapon': weapon, 'opponent_weapon': opp_weapon, 'message': message}

    return result

Now when we visit /shoot/bazooka we get a much more descriptive error message.

There’s one last issue I’d like to address before deploying this puppy to Deta.. Here we’re using what’s called a path parameter to input the weapon. Technically this works fine, but it’d be better to use a query parameter. “rock” describes a weapon. It’s more of an attribute than an instance of a weapon. Potentially we could expand our game to include things like weapon color and weapon size. Imagine the mess that would create if we kept using path parameters.. People would hit endpoints like

/shoot/rock/red/3 or
/shoot/paper/2/blue.

There’s no natural hierarchy to these attributes and thus no intuitive order for building a path. A better URL structure would look like

/shoot?weapon=rock&color=red&size=3 or
/shoot?weapon=paper&size=2&color=blue.

So it makes sense to change weapon from a path parameter to a query parameter. To do this, we simply change

@app.get('/shoot/{weapon}')
async def shoot(weapon: Weapon):
    ...

to

@app.get('/shoot')
async def shoot(weapon: Weapon):
    ...

Now if we visit /shoot we get {"detail":[{"loc":["query","weapon"],"msg":"field required","type":"value_error.missing"}]}

but if we visit /shoot?weapon=rock we get something like {"user_weapon":"rock","opponent_weapon":"paper","message":"You lost."}.

Deployment (version 2)

Now let’s deploy our updated app. Deploying our updates to Deta is as simple as running deta deploy (keeping in mind we would’ve needed to update requirements.txt if we had incorporated other packages).

I’ll also set up a subdomain for my app within the Deta dashboard so you can visit my app at https://rockpaperscissors.deta.dev/.

Assuming I haven’t deleted the app by now, you can try it out for yourself.

Lastly, check out the awesome swagger documentation we get for our REST API for free thanks to fastapi.