API Skeleton

Part 1, Chapter 4

What Are We Building?

Let's dive a bit deeper into the application that we're building. You have more than enough things to do each day, right? It's hard for you to keep track, and all the tools seem too complicated to use. Thus, you've decided to build yourself an application that will allow you to easily manage your tasks.

Your initial idea is to cover these cases:

  • create a task
  • list open tasks
  • list closed tasks
  • close a task

Where to Start?

The most natural instinct is to open your favorite code editor and start coding. After that you'd start thinking about deployment. You'd deploy it to some server. And then what? New ideas would come. You'd change some code, and add some new code. Are you sure you didn't break anything? Hmm. You're probably not 100% certain, so you'd spend twenty or so minutes testing it manually. That covers just the happy path, so you'd spend another thirty minutes trying to manually test all the edge cases. Once everything works, you'd deploy it... but wait... how did you deploy it? Do you remember the exact steps that you did for the first deploy? Do you see where I'm going with this? Fortunately we know a better way to develop software.

Rather than diving right in, let's focus less on the application requirements and more on it's skeleton and CI/CD pipeline. Along the way, we'll also create resources needed to deploy our application. We'll start implementing ideas only after the circle is completed.

Create a Service

Let's start with our backend service. Move to your project directory, and create a new folder called "services":

$ cd tasks
$ mkdir services

Within "services", create a new folder called "tasks_api":

$ cd services
$ mkdir tasks_api

Initialize a project with Poetry inside "tasks_api":

If you don't have poetry installed follow the official installation guide.

$ cd tasks_api
$ poetry init

Package name [tasks_api]:
Version [0.1.0]:
Description []:
Author [Your name <[email protected]>, n to skip]:
License []:
Compatible Python versions [^3.11]:  ^3.9

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Do you confirm generation? (yes/no) [yes]

We're using Python 3.9 because, as of writing, that's the latest version supported by AWS Lambda

Next, install the following development tools:

$ poetry add --dev pytest pytest-cov black isort flake8 bandit

These tools will be used to help us produce high quality software.


  • pytest - used for writing and running automated tests
  • Black - takes care of code formatting
  • isort - optimizes imports
  • Flake8 - checks for PEP8 compliance
  • Bandit - checks for code security vulnerabilities

You can learn more about these tools in Python Code Quality, Testing in Python, and Modern Test-Driven Development in Python

Next, add the pyproject.toml and poetry.lock files to git, create a commit, and push your code:

$ git add pyproject.toml poetry.lock
$ git commit -m 'Create tasks_api service'
$ git push -u origin main

FastAPI Application

Since your application needs to be accessible from a web browser, we need a web framework. Let's go with FastAPI.

First, install FastAPI:

$ poetry add fastapi uvicorn httpx

Uvicorn is a lightning-fast ASGI server implementation used to serve FastAPI applications

HTTPX is fully featured HTTP client with aysnc and sync APIs. FastAPI's TestClient, used to write API tests, is based on it. See FastAPI's official docs for more details.

Next, let's add tests which will run inside our CI/CD pipeline. We'll start with a simple health check endpoint, starting with a test. Create a new module called tests.py inside "services/tasks_api":

import pytest
from fastapi import status
from starlette.testclient import TestClient

from main import app

def client():
    return TestClient(app)

def test_health_check(client):
    WHEN health check endpoint is called with GET method
    THEN response with status 200 and body OK is returned
    response = client.get("/api/health-check/")
    assert response.status_code == status.HTTP_200_OK
    assert response.json() == {"message": "OK"}

client is a pytest fixture. A fixture is function that's executed by the pytest runner. By default, fixtures run before each test, but this can be changed via the scope argument.

Its returned value is passed to the test function's client argument. The name of fixture and argument must be the same.

We added the GIVEN, WHEN, THEN notation to the test:

  1. GIVEN tells you about the initial conditions/context.
  2. WHEN tells you what's occurring that needs to be tested.
  3. THEN tells you what's the expected outcome.

Such docstrings improve the readability of your tests.

Want to learn more about fixtures and the GIVEN, WHEN, THEN notation? Check out the Modern Test-Driven Development in Python article.

Ensure the test fails:

$ poetry run pytest tests.py


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

After that, create a new file called main.py inside "services/tasks_api":

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

app = FastAPI()

def health_check():
    return {"message": "OK"}

That's it. Simple as that. Just return {"message": "OK"}.

We added CORSMiddleware with completely open configuration to simplify our development -- so we can consume API from Vue application locally. It adds needed CORS headers to responses. You can learn more about it in the official docs.

The test should now pass:

$ poetry run pytest tests.py

At the end, add all changes to git, create a commit, and push:

$ git add -A
$ git commit -m 'Application skeleton is set up'
$ git push -u origin main

You now have running application that's tested.

Want to test it out? Run poetry run uvicorn main:app --reload. Then, navigate to in your browser. You should see {"message": "OK"}.

Small, Incremental Changes

Thus far, you've built an application that returns {"message": "OK"} when you call /api/health-check/. It may not seem like much but you actually have a working application. You have a code base with tests that can run inside a CI/CD pipeline. You have an application that you can deploy and monitor. That's perfect for now.

So, what comes next? Before we answer that, let's think about what you want to achieve first. You want to build an application that will solve your problem. You don't want to create new ones. You want your application to run as smooth as possible. You probably also want to build an application that will be able to follow changes in your business model.

It's easier said than done. Nevertheless, we know approaches and tools that can help us achieve all of these: Test-driven Development (TDD) and Continuous Integration and Delivery (CI/CD).

In order to practice these, we need to focus on making small, incremental changes to the code base. Such changes are easier to control and manage.

A perfect example of this is numerical integration. Say you have a function f(x) and want to calculate the integral from point a to point b. If you take a big step you'll create a huge error:

Big changes

On the other hand, the error will be much, much smaller if your steps are small:

Small changes

It's similar with the size of changes during software development. If changes are small, you can adapt quickly. On the other hand, if changes are big, you may not realize just how far off you are from reality until the changes are completed and live in production.

To answer the initial question of what comes next, we'll set up a CI/CD pipeline for our current application. We'll set up CI/CD jobs for checking code quality, running our automated tests, and deploying our application. Again, we'll do this incrementally, step-by-step. Once done, only then will we start building out the application.

What Have You Done?

In this chapter you added a new service called tasks_api, installed FastAPI via Poetry, and added a health check endpoint covered by a test. At this point, your project should look like this:

├── .gitignore
└── services
    └── tasks_api
        ├── main.py
        ├── poetry.lock
        ├── pyproject.toml
        └── tests.py

Mark as Completed