#220: Manage To-Dos With FastAPI
After covering the basics of FastAPI, it is now time to do something more elaborate. For that purpose, we create a minimalistic to-do task tracker that let us work against an in-memory data store. To save us the time to constantly click in the browser, we will do this exercise by first writing a test and then create the implementation using the test-first approach.
The Pydantic models
Four our little application we split the input models from those we send back to the API client. That allows us to be specific on what data we expect from the users and show the difference to what we keep inside the API.
Our input model has these four fields:
What we send back to the client contains a bit more details:
A failing test to start
We want to work with our API by sending a task to the API and get the data with some additional details back:
Our test currently fails and will so for the next few steps that we need to get done so that we get enough code to run our API.
A new API application
Our minimalistic first attempt to make our test pass can look like this:
Make sure that you create the empty __init__.py files inside the models and tests folder and put one next to main.py. Otherwise, you end up with an error message like this one:
ImportError while importing test module ‘….\tests\test_todo.py’.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
…
tests\test_todo.py:3: in
from ..main import app
E ImportError: attempted relative import with no known parent package
If we now run our test, we should see it passing.
A failing test to add more functionality
With our first test passing, we can go for the next feature: we want to get the details of a newly created task. With this test we can check if our API gives us the expected data:
This test also fails at first, then currently there is no endpoint and no way to store the data between the requests to get the result we expect.
Add a mock data store
To make our second test pass, we need to keep the data around for two (or more) requests. We can do that with a variable in the API that works as a mock data store:
We need to change the create_task() method to put the newly created task into our data store and then read from there when someone asks for the item in the show_task() method:
We can now run our two tests, and both will pass.
Add some error handling
What happens if we ask for a task that does not exist? Will our application crash or will it produce a useful message? Let us write a test to find out what happens:
Our current implementation will throw an exception because there is no element at position 0 in an empty list. We can make our test pass by adding this check:
The HTTPException allows us to specify a status code and set a message that we can send to the client. With that addition, our API can now handle the case where the requested task does not exist.
Update a task
We currently can create a task and get its details. When we want to update our task, we need a new test to drive our functionality:
In our API, we need to find the requested task, update the fields, and then return the changed task:
Attention: Since we work on the element of the list and not on a copy, we do not need to update the list. That will need to change when we use a database.
We can now run our tests and they all should pass. We can spot an opportunity for refactoring when it comes to the duplicated code to create an event to have some data to work on in our tests. We write that down to a notepad but postpone that refactoring for a moment.
Delete a task
When we want to delete a task, we start once more with a failing test:
We can now implement the delete_task() method with this code:
When we do not specify a response, we get the default response with status code 200. That currently is good enough for our API. We will return to status codes in a future post.
Run with uvcorn
If we run our to-do app like we did the minimalistic example, we run into this error:
File “…\minimal_todo\main.py”, line 3, in
from .models.todo import *
ImportError: attempted relative import with no known parent package
Instead, we need to go one folder up of our main.py and run uvicorn with this command:
If we go to http://127.0.0.1:8000/docs, we should see our Swagger documentation:

Next
With our test-first approach we got our ToDo API in a state that it supports the basic CRUD operations. The tests help us to make sure that the application keeps working when we add more features or change the behaviour. However, there is now a lot of redundancy in our tests that we can refactor next week.