Python FastAPI crash course: Make your first CRUD API

ericchapman

Eric The Coder

Posted on November 23, 2021

Python FastAPI crash course: Make your first CRUD API

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: https://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},
]
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Read all products

GET www.example.com/products
Enter fullscreen mode Exit fullscreen mode

Read a particular product (e.g. with id = 2)

GET www.example.com/products/2
Enter fullscreen mode Exit fullscreen mode

Modify a specific product (e.g. with id = 2)

PUT www.example.com/products/2
Enter fullscreen mode Exit fullscreen mode

Delete a specific product (e.g. with id = 2)

DELETE www.example.com/products/2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

To start the server and test your API, type in the terminal (if you haven't already done so).

$ uvicorn first-api:app --reload
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You will probably get this error

{
  "detail": [
    {
      "loc": [
        "path",
        "id"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's all for today, follow me on twitter: https://twitter.com/EricTheCoder_ to be notified of the publication of the next article (within two days).

💖 💪 🙅 🚩
ericchapman
Eric The Coder

Posted on November 23, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related