Create a fast auto-documented, maintainable and easy-to-use Python API — CRUD routes and routing…
Learn how to catch requests and obtain the information that they carry
Create a fast auto-documented, maintainable and easy-to-use Python API — CRUD routes and routing management
In the previous article we’ve set up an API in 5 lines of code. We’ve install our dependencies and created a working API with just one simple route. In this article we’ll build further, fleshing out the API with multiple, different routes that’ll show you all the things you can do with FastAPI. Then we’ll focus on how to organize all of these routes in a concise and clear way
Setting up
To make this article as understandable as possible we’re going to pretend we’re coding an API for our blogging website. The goal is to create some common routes with which we can CRUD (create, read, update, delete):
- add new article
- retrieve articles from the database
- update articles to fix typo’s e.g.
- delete an article
We’ll focus on catching the requests and extracting the information. How our API communicates with our database is another topic that’s described perfectly well in this article. Also check out this article for creating a version controlled database structure.
Since our API will be responsible for catching the requests and obtaining the information that they carry, let’s first quickly check out how request work so that we can accurately create endpoints for our API to catch all the information.
The request
Let’s simplify a bit and say that a request consists of a URL
, a HTTP method
and optionally a body
. We’ll go through each of these quickly before implementing them in our API.
Dissecting a URL
I assume you are quite familiar with URLs since you have to type them into a browser when using the internet. For the sake of our API let’s split the URL below in the 3 parts that are relevant for us.
https://www.github.com/mike-huls?tab=overview&lang=en
- https://www.github.com
This part specifies the location of the server where the website is hosted. - /mike-huls
The path. This specifies the exact location of the resource (file) that we want to access on the website. Think of this as a file system on your computer; just likec:/users/mike/mystuff
. - ?tab=overview&lang=en
The question mark is the query string separator. It separates the routing to a specific resource from the query parameters that will be passed to the resource. Parameters are passed in pair, separated by a ‘&’.
In summary: we send a request to https://www.github.com
, search for our resource on that server at /mike-huls
and then pass the resource some query parameters, namely tab=overview
and lang=en
. We’ll use these terms later when building routes.
HTTP Methods
A method (also called a ‘verb’) is used to distinguish different (HTTP) operations when sending requests. Though there are many more, for this article we’ll use the main 4:
- GET— for retrieving data
- POST — for submitting data
- PUT— for updating data
- DELETE — for deleting data
I’d like to think of these methods as nothing more than extra data to distinguish your goal.
Request body
The body is the actual data that we want to send. In our case we can POST a new article so that our API can insert it into the database.
Creating our API
Remember that requests consist of a URL and a method. To catch these we need to create specific routes for the different types of requests. In a route we need to specify (at least) two things:
- a path (to catch the path of the URL
- a method (to catch the HTTP method of the request)
Let’s go through all the required operations for Creating, Reading, Updating and Deleting articles (CRUD). We’ll start with the easiest: retrieving articles.
Simple READ: Route for retrieving all articles
The simplest route is one for retrieving all articles. It looks like this:@app.get("/all_articles")
def get_all_articles():
return function_that_retrieves_all_articles()
Two things are important. First notice that we’re using the GET method (signified by @app.get
). The second is where we specified our path: the articles
part in the GET method. This defines the path to this route. A get request to this route may look like: www.ourbloggingwebsite.com/all_articles
which will retrieve all of our articles from the database.
READ with query parameter: Route for retrieving one article
Retrieving all articles is pretty nice for getting an overview of all of our articles but what if the user wants to read just one? Let’s create another route for retrieving just one article:@app.get("/article")
def get_one_article(articleId:int):
return function_that_retrieves_article(articleId=articleId)
Notice that the route look a lot like the previous one. It still uses a get method but is has a slightly different path: the function now requires an argument; we have to provide an articleId. This is called a query parameter. A GET request to this route looks like this: www.ourbloggingwebsite.com/article?articleId=1
. This returns just one article, namely, the one where articleId =1.
We can add to this route a bit more like this@app.get("/article")
@app.get("/article/{articleId}")
def get_one_article(articleId:int):
return dbConnection.get_one_article(articleId=articleId)
Notice that we’ve added the second line here; it allow users to both retrieve an article with query parameters like /article?articleId=1
and with a path like /article/1
. We can use both line or just one.
DELETE: Route for deleting an article
The delete statement works exactly the same as the GET with a query parameter, it uses query parameters to filter.@app.delete("/article")
def delete_one_article(articleId:int):
return dbConnection.delete_one_article(articleId=articleId)
As you can see it’s pretty similar to the previous method, the only exception being that we use the DELETE method in the first line.
CREATE: Route for posting a new article
Let’s get to the exiting stuff; inserting a new article. The route looks like this:@app.post("/article")
def post_article(body:dict):
return dbConnection.post_article(articleJson=body)
Notice again that we’re using a POST that we send to our /article route. FastAPI takes the body data from the request and passes it to the function that’s responsible for inserting it into the database.
UPDATE: Route for updating an existing article
The update combines the query parameters from the query parameters from the GET and the body from the POST request:@app.put("/article")
@app.put("/article/{articleId}")
def update_article(articleId:int, body:dict):
return dbConnection.update_article(articleId=articleId, articleJson=body)
Notice that it receives an articleId and a body. These we can use to replace a record in our database; we look it up using the articleId and then replace it with the data in articleJson. Easy!
Organizing routes
All of the routes that we’ve just created are just for one thing: modifying articles. What if we also have stories? With all of these routes you can imagine that your projects gets cluttered pretty fast. Thankfully organizing these routes is easy.
We’ll create a file called article_stories.py
and add the following code:from fastapi import APIRouter
router_stories = APIRouter()
After we’ve added this code we can use the router to catch the requests. We’ll just take all the pieces of code we’ve written in the previous chapter and paste ‘m in article_stories.py
.@router_stories.get("/")
def get_one_story(story_id:int):
return function_that_retrieves_story(story_id=story_id)
Those with a keen eye for detail noticed one thing: in the previous part we’ve decorated the function with @app.get(....)
. In the example above we have to use @router_stories
since this is our new router. Check out the final version of this file here.
The last step is to go to our main.py
file and tell it to redirect certain requests to our new file. We redirect all requests that have something to do with article to our articles router by importing it and telling FastAPI to redirect certain requests. Our main.py file is now nice and short:from fastapi import FastAPI
from routes.stories_routes import router_stories
app = FastAPI()
app.include_router(router=router_stories, prefix='/articles')
When we start handling stories we can easily include a new router and write all of our required code in there! Easy and organized.
Conclusion / TL;DR
Check out the repo with the final api here.
This article focused mainly on what a request is and how we can get different types of information from these requests in our routes. With this knowledge you can already create a very nice API. In the next article we’ll focus on defining models for our routes. With these we can clean and check data before passing it on to the database. Also we’ll focus on security; e.g. only authenticated access or you have to provide a password. Subscribe to stay tuned!
If you have suggestions/clarifications please comment so I can improve this article. In the meantime, check out my other articles on all kinds of programming-related topics.
Happy coding!
— Mike
P.S: like what I’m doing? Follow me!