Here is a crash course (series of articles) that will allow you to create an API in Python with FastAPI.
I will publish a new article about every two days and little by little you will learn everything there is to know about FastAPI
To not miss anything follow me on twitter: twitter.com/EricTheCoder_
Create a CRUD API
Now we are going to create an API that will be closer to what you will need to create in a real project.
CRUD is an acronym that stands for Create, Read, Update, and Delete. These actions are the actions most often used when manipulating data.
Here is a concrete example. Consider a data table containing products:
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
So you might have URL paths to perform CRUD actions on this product board.
Here are some examples:
Create a new product
POST www.example.com/products
Read all products
GET www.example.com/products
Read a particular product (e.g. with id = 2)
GET www.example.com/products/2
Modify a specific product (e.g. with id = 2)
PUT www.example.com/products/2
Delete a specific product (e.g. with id = 2)
DELETE www.example.com/products/2
Note that the name and structure of URL paths are not random. It is a convention that is used in the creation of APIs.
This is why to retrieve a particular product you must specify its id directly in the path:
GET www.example.com/products/2
FastAPI allows you to read this path and extract the relevant information. We will see this concept shortly.
First step
In your file first-api.py replace the current content with this one
from fastapi import FastAPI
app = FastAPI()
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
@app.get("/products")
def index():
return products
To start the server and test your API, type in the terminal (if you haven't already done so).
$ uvicorn first-api:app --reload
So you can then visit: http: //127.0.0.1: 8000/products
The list of all products will be displayed in JSON format:
[
{
"id": 1,
"name": "iPad",
"price": 599
},
{
"id": 2,
"name": "iPhone",
"price": 999
},
{
"id": 3,
"name": "iWatch",
"price": 699
}
]
So we created the READ of our CRUD API. Now let's see the other URL paths
Extract the "id" from the URL path
To read all a particular product we need to extract the id from the url path. For example with the path "/products/2" how to extract the 2?
FastAPI allows to automatically send part of the path in a variable
@app.get("/products/{id}")
def index(id: int):
for product in products:
if product["id"] == id:
return product
return "Not found"
In the @app.get() the part represented by {id} will be sent in the variable "id" of the function index(id: int)
It is then possible to use this "id" variable to find the right product.
Note that the "id" parameter is completed with ":int" This addition allows you to specify the type of the variable, in this case an integer.
Why use a type in the parameter? This allows FastAPI to validate the incoming data.
For example the path "/products/abc" would return an error because "abc" is not an integer
Status Code
When the HTTP server returns a response, it always returns a status code with the response.
All HTTP response status codes are separated into five classes or categories. The first digit of the status code defines the response class, while the last two digits have no ranking or categorization role. There are five classes defined by the standard:
1xx information response - request has been received, process in progress
2xx successful - the request was received, understood and accepted successfully
3xx redirect - additional steps must be taken in order to complete the request
Client error 4xx - request contains bad syntax or cannot be fulfilled
Server error 5xx - the server failed to respond to an apparently valid request
Here are some examples of status code
200 OK
201 Created
403 Forbidden
404 Not Found
500 Internal Server Error
In our last FastAPI example, if the product is not found the path will return "Not found" however the status code returned will always be "200 OK"
By convention when a resource is not found it is necessary to return a status "404 Not Found"
FastAPI allows us to modify the status code of the response
from fastapi import FastAPI, Response
...
@app.get("/products/{id}")
def index(id: int, response: Response):
for product in products:
if product["id"] == id:
return product
response.status_code = 404
return "Product Not found"
To do this we must add 3 lines to our code:
- First we need to import the Response object
- Then add the "response: Response" parameter to our function
- And finally change the status to 404 if the product is not found
Note that the "response: Response" parameter may seem strange to you, indeed how is it possible that the "response" variable contains an instance of the "Response" object without even having created this instance?
This is possible because FastAPI creates the instance for us in the background. This technique is called "Dependency Injection".
No need to understand this concept, just using it is enough for now.
Extract the "Query Parameters"
Take for example the following path:
/products/search/?name=iPhone
This path requests the list of all products which contain the word "iPhone"
The "?Search=iPhone" is a Query Parameter.
FastAPI allows us to extract this variable from the URL path.
After the existing code, enter:
@app.get("/products/search")
def index(name, response: Response):
founded_products = [product for product in products if name.lower() in product["name"].lower()]
if not founded_products:
response.status_code = 404
return "No Products Found"
return founded_products if len(founded_products) > 1 else founded_products[0]
Just add the name of the variable as a parameter to the index() function. FastAPI will automatically associate Query Parameters with variables of the same name. So "?Name=iPhone" will end up in the parameter/variable "name"
Path declaration order
If you launch your server and try to visit the following URL path
http://127.0.0.1:8000/products/search?name=iPhone
You will probably get this error
{
"detail": [
{
"loc": [
"path",
"id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
Why ? The error message states that the value is not of type integer? Yet if we look at our function, no value is of type integer? We only have a "name" parameter
In fact, the message speaks the truth. Here is all our code so far
from fastapi import FastAPI, Response
app = FastAPI ()
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
@app.get("/products")
def index():
return products
@app.get("/products/{id}")
def index(id: int, response: Response):
for product in products:
if product["id"] == id:
return product
response.status_code = 404
return "Product Not found"
@app.get("/products/search")
def index(name, response: Response):
founded_products = [product for product in products if name.lower() in product["name"].lower()]
if not founded_products:
response.status_code = 404
return "No Products Found"
return founded_products if len(founded_products) > 1 else founded_products[0]
We have a route "/products/{id}" which is declared before the last route. The dynamic part of route "{id}" means that all routes that match "/products/*" will be executed with this code.
So when we ask for "/products/search/?Name=iPhone" FastAPI sends us to the second route because it matches "/products/*". The last function is never performed and never will be.
The solution? Reverse the routes, the order of the routes is essential for FastAPI. it is therefore important to place dynamic routes like "/products/{id}" last
from fastapi import FastAPI, Response, responses
app = FastAPI()
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
@app.get("/products")
def index():
return products
@app.get("/products/search")
def index(name, response: Response):
founded_products = [product for product in products if name.lower() in product["name"].lower()]
if not founded_products:
response.status_code = 404
return "No Products Found"
return founded_products if len(founded_products) > 1 else founded_products[0]
@app.get("/products/{id}")
def index(id: int, response: Response):
for product in products:
if product["id"] == id:
return product
response.status_code = 404
return "Product Not found"
With the code in that order, if you revisit "/products/search?Name=iphone". You will have the following answer:
{
"id": 2,
"name": "iPhone",
"price": 999
}
Conclusion
That's all for today, follow me on twitter: twitter.com/EricTheCoder_ to be notified of the publication of the next article (within two days).