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.
Details:
- 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
@pytest.fixture
def client():
return TestClient(app)
def test_health_check(client):
"""
GIVEN
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:
GIVEN
tells you about the initial conditions/context.WHEN
tells you what's occurring that needs to be tested.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
Failure:
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()
app.add_middleware(
CORSMiddleware,
allow_origins="*",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health-check/")
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 http://127.0.0.1:8000/api/health-check/ 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:
On the other hand, the error will be much, much smaller if your steps are small:
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