This page looks best with JavaScript enabled

A TDD Approach to Creating an Authentication System with FastAPI, Part 1

 ·  ☕ 7 min read

Introduction

I have been tinkering with this new framework and have a little bit of experience with FastAPI as of now. What I’ve though of is to do a post about authentication and authorization. I’m sure going to do a registration and login system and nothing fancy this time. I am going to use JWT as authorization mechanism. This is mostly a walkthrough of new features in Python.

I will not go in-depth in any of the topics, but will provide you with links which you can dig up to expand your knowledge.

Series Index

  • Project Setup and FastAPI introduction (you are here)
  • Database Setup and User Registration
  • Mocking the Database

Prerequisite

This post is fairly easy and even beginners can follow it. Following are some packages which I will be using in this post.

  • Python as a language
  • pytest as a test runner
  • FastAPI as application layer
  • PostgreSQL as database layer
  • psycopg2, the database connector
  • SQLAlchemy for ORM
  • Docker for PostgreSQL

Make sure Python and Docker are installed on your system. You should be able to do a docker run hello-world and python -V without error. Please avoid installing Python 2. Leave a comment if you are stuck, then I can do something.

If you are following this tutorial on Windows, I expect you to be working in WSL. On Mac, there should be no issue. For every other installation, follow along with me. Feel free to connect with me on LinkedIn or drop a “Hi” on Twitter.

Setting up the project

Setup a virtual environment

Issue this command to your terminal.

$ python3 -m venv .venv

You should get back the prompt after a few seconds, depending on how fast your system is.

You should be able to see a .venv directory like shown below.

$ ls -la
total 12
drwxrwxr-x  3 ec2-user ec2-user 6144 Nov 11 13:12 ./
drwxrwxr-x 21 ec2-user ec2-user 6144 Nov 11 12:56 ../
drwxrwxr-x  5 ec2-user ec2-user 6144 Nov 11 13:12 .venv/

To get into the virtual environment, do:

$ source .venv/bin/activate

The prompt will be prepended with (.venv). In my case, my customized full prompt looks like this:

(.venv) ec2-user at ip-10-2-1-250 in ~/workspace/fastauth $ 

As you can see, I have created a directory called fastauth, which is root of this project. Please also take note that from now on, whenever I run any command, I’m inside this virtual environment. So you should be too.

Hello World in FastAPI

Let’s take a TDD approach and write our test first. The requirements of our first endpoint are:

  • Request to /ping should respond with a HTTP 200 status.
  • Request to /ping should respond with a custom JSON: {"msg": "pong"}.

Let’s write our first test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from fastapi.testclient import TestClient

from main import app

client = TestClient(app)

def test_ping():
    response = client.get("/ping")
    assert response.status_code == 200
    assert response.json() == {"msg": "pong"}

Save above to a file called test_main.py.

Run the test

To run the test, you need to have pytest installed.

pip install pytest

Now run the test..

$ pytest
=========================== test session starts ===========================
platform linux -- Python 3.7.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /efs/repos/fastauth
collected 0 items / 1 error                                                           

================================= ERRORS ==================================
______________________ ERROR collecting test_main.py ______________________
ImportError while importing test module '/efs/repos/fastauth/test_main.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib64/python3.7/importlib/__init__.py:127: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
test_main.py:1: in <module>
    from fastapi.testclient import TestClient
E   ModuleNotFoundError: No module named 'fastapi'
========================= short test summary info =========================
ERROR test_main.py
!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!
============================ 1 error in 0.17s =============================

The first error we would receive is this ModuleNotFoundError with a message No module named 'fastapi'. That’s because we really don’t have anything in our virtual environment other than pytest.

We are doing good with our TDD approach. We are following the red, green, refactor approach. In this approach we first write test to fail the test. Then we write the minimal code to pass the test (green resembles passed test). In third step we refactor redundant code.

Moving forward, let’s get rid of this ImportError by installing fastapi in our virtual environment and run the test again.

$ pip install fastapi
[..output trimmed..]

$ pytest

This time I’m not going to show you the entire output, but the final line says:

import requests
ModuleNotFoundError: No module named 'requests'

That a red again.

Let’s install the required package and try again.

$ pip install requests
[..output trimmed..]

$ pytest

The error this time is different:

from main import app
ModuleNotFoundError: No module named 'main'

That’s because we haven’t created the main.py file yet. Let’s go ahead and create one and run the test again:

$ touch main.py

$ pytest

from main import app
ImportError: cannot import name 'app' from 'main' (.../fastauth/main.py)

Minimal code to pass the test

We are yet again going to write minimal code to pass this test. Open up a file called main.py and write this:

1
2
3
from fastapi import FastAPI

app = FastAPI()

Let’s run the test this time:

$ pytest -q
F                                                           [100%]
============================ FAILURES =============================
____________________________ test_ping ____________________________

    def test_ping():
        response = client.get("/ping")
>       assert response.status_code == 200
E       assert 404 == 200
E        +  where 404 = <Response [404]>.status_code

test_main.py:9: AssertionError
===================== short test summary info =====================
FAILED test_main.py::test_ping - assert 404 == 200
1 failed in 0.78s

Now this time we get some real error. We are getting a HTTP 404 error, that’s because that endpoint is not defined yet. That makes me really happy. I got some real error.

I will go ahead and add route and route handler:

1
2
3
4
5
6
7
from fastapi import FastAPI

app = FastAPI()

@app.get("/ping")
async def ping():
    return 'Hello World!'

Some explanation of the code now:

  1. What are can do with app is, we can wrap a handler function with it. The handler function above simply return the string 'Hello World'.
  2. Let’s look at the @app.get("/ping") part now. /ping is the route here. get is the GET operation on the endpoint.

How about running the test again?

$ pytest -q
F                                                                                            [100%]
============================================= FAILURES =============================================
____________________________________________ test_ping _____________________________________________

    def test_ping():
        response = client.get("/ping")
        assert response.status_code == 200
>       assert response.json() == {"msg": "pong"}
E       AssertionError: assert 'Hello World!' == {'msg': 'pong'}
E        +  where 'Hello World!' = <bound method Response.json of <Response [200]>>()
E        +    where <bound method Response.json of <Response [200]>> = <Response [200]>.json

test_main.py:10: AssertionError
===================================== short test summary info ======================================
FAILED test_main.py::test_ping - AssertionError: assert 'Hello World!' == {'msg': 'pong'}

Yeah! Seems like the first assertion passed. What it did not pass is the response message.

Let’s correct that.

5
6
7
@app.get("/ping")
async def ping():
    return 'Hello World!'

And the output:

$ pytest -q
.                                                   [100%]
1 passed in 0.53s

Related reading: https://fastapi.tiangolo.com/tutorial/testing/

FastAPI has support for OpenAPI, builtin.

One good thing among many about FastAPI is that, you don’t have to install Swagger separately. You can import and export the specs without much hassle. Let’s see what FastAPI has to offer. But before that, let’s start our development server.

We were, till now, not running the actual server. But were testing the routes. To run the actual server, we need a companion package of FastAPI called uvicorn.

pip install uvicorn

and then run this command:

$ uvicorn main:app --reload

Let’s see what it does.

  1. uvicorn is the command to start the ASGI server interface.
  2. What we are passing to uvicorn is main:app which is the module:app (app here is the FastAPI instance).
  3. Basically what --reload does is, it reloads the new content from disk when file is updated on disk.

Below is the mine output.

INFO:     Will watch for changes in these directories: ['/home/ec2-user/workspace/fastauth']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [20670] using statreload
INFO:     Started server process [20672]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

As you can see the output from the console, our server started on http://127.0.0.1:8000. So we should be able to see our ping response on http://127.0.0.1:8000/ping

ping response from the server
ping response from the server

What fun thing you could do with FastAPI is, nagivate to the SwaggerUI at http://127.0.0.1:8000/docs.

Swagger UI at /docs
Swagger UI at /docs

And play around:

Play around with request and response
Play around with request and response

You may want to download the OpenAPI spec from http://127.0.0.1:8000/openapi.json if you wish. This file will get populated as we follow this tutorial/series.

In next section in this series, I’ll show you how to setup database and proceed with user registration.

Share on

Santosh Kumar
WRITTEN BY
Santosh Kumar
Fullstack Developer at Method Studios