Eric The Coder
Posted on November 29, 2021
Here is a crash course (series of articles) that will allow you to create an API in Python with FastAPI.
To not miss anything follow me on twitter: https://twitter.com/EricTheCoder_
POST
So far we have covered the READ of CRUD, now let's see the "Create".
Here it is no longer a question of reading data but of creating a new one.
To create a new product we need to send some information to the server, and to do this we will need to use the HTTP POST action
POST www.example.com/products
The POST action sends data from the client to the server.
Example
We will do an example and add the product "MacBook" to our existing product list.
Here is the expected result:
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
{"id": 4, "name": "MacBook", "price": 1299},
]
To do a POST with FastAPI here is the code you must use
@app.post("/products")
def create_product(product: dict, response: Response):
product["id"] = len(products) + 1
products.append(product)
response.status_code = 201
return product
Let's take this code line by line:
@app.post("/products")
This code is used to specify to FastAPI that this request will be a POST "/products" action
def create_product(product: dict, response: Response):
This code is used to define the function that will be launched when the server receives the "POST/products" request.
The function contains two parameters, one for the product and the other for a reference to the response object.
Note that when performing a POST action, the data to be sent must be included in the body of the request and must be in JSON text format.
In this example we see all the power and ease of FastAPI. FastAPI knows it is a POST request, so it will automatically take the JSON that is in the request body, convert it to a Python dictionary and place the result in the first parameter (product).
So we can then use this "product" parameter to add the product to our product list.
product["id"] = len(products) + 1
products.append(product)
Once the product has been added successfully, the appropriate status code should be returned.
response.status_code = 201
This code is used to return the status code 201 (Created). Which indicates that the product is indeed created successfully.
Finally, by convention, the new product is always returned with the response.
return product
Testing an HTTP Post action
Web browsers do not allow you to initiate a POST request. So it will not be possible to test this new API from the browser.
There are several other ways to do this. We could use software like Postman and test our requests from Postman.
One of the advantages of FastAPI is that it automatically generates documentation for our API. This documentation allows users to understand how to use our API, what are the available URL paths and also to execute requests.
We will therefore be able to use the documentation to test our API
As mentioned, in the case of our API the documentation is created automatically. To view it, start the server
$ uvicorn first-api:app --reload
and visit /docs
http://127.0.0.1:8000/docs
The documentation will be displayed on the screen!
To test a URL path it's very simple, just click on the path name and FastAPI will reveal an interface allowing you to test this endpoint.
For example if you click on POST /products you will see appear
You can then click on "Try it out", enter the data to add in the "Request body" section (see example below)
The data is in JSON text format
{"name": "MacBook", "price": 1299}
Finally click "Execute", the doc send the POST request to your FastAPI server
The response status code as well as the response itself will also be displayed on the page.
This automatic documentation is yet another great demonstration of the power and ease of FastAPI.
If you wish, you can click on all the endpoints one after the other in order to test them.
Validation
Let's go back to the last example we created
@app.post("/products")
def create_product(product: dict, response: Response):
product["id"] = len(products) + 1
products.append(product)
response.status_code = 201
return product
The @app.post() function works but has several shortcomings. In fact, there is no validation and no error message is returned if this validation does not pass.
For example, to create a new product we need the "name" and "price". What happens if only the "name" is sent but not the "price"?
Or what if the "price" is not a number format.
I could give you several more examples but I think you understand the concept.
To make these validations and return the associated error messages, we would have to add a lot of code to our function. And this code would have to be repeated for each action and each resource. All of this would greatly complicate our application.
Fortunately, FastAPI is named "Fast" API. So there is a very simple way to implement an automatic validation system. This system is the schema!
The schema
Schemas are data models that FastAPI uses to validate our functions.
For example we could define a schema like this:
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: float
This data model is very easy to understand. We have a "Product" entity which contains the "name" and "price" attributes. We can even define what the type attributes are.
Once we have defined our data model, we can modify our @app.post() function
@app.post ("/products")
def create_product(new_product: Product, response: Response):
product = new_product.dict()
product["id"] = len(products) + 1
products.append(product)
response.status_code = 201
return product
We replaced the "dict" type parameter with a "Product" type parameter.
Once this change is made, if you test this function, you will see that FastAPI now applies automatic validation and will return an error if what you send does not respect the "Product" model.
PUT
The PUT action is used to modify an existing resource. Like the POST action, the data to be modified must be sent with the request.
Here is an example of a PUT function
@app.put("/products/{id}")
def edit_product(id: int, edited_product: Product, response: Response):
for product in products:
if product["id"] == id:
product['name'] = edited_product.name
product['price'] = edited_product.price
response.status_code = 200
return product
else:
response.status_code = 404
return "Product Not found"
There is nothing really new here. These are exactly the same concepts that we discovered a little earlier.
DELETTE
The DELETE action allows you to delete a resource. Here is the code to implement this action
@app.delete("/products/{id}")
def destroy_product(id: int, response: Response):
for product in products:
if product["id"] == id:
products.remove(product)
response.status_code = 204
return "Product Deleted"
else:
response.status_code = 404
return "Product Not found"
Database
The code you have just created to implement CRUD is fine, but it still lacks an important notion and that is to link the resource with a database. The next section will explain how to do it step by step.
Conclusion
That's all for today, follow me on twitter: https://twitter.com/EricTheCoder_ to be notified of the publication of the next article.
Posted on November 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.