Using FastAPI to Build Python Web APIs

 Table of Contents

Creating APIs, or application programming interfaces, is an important part of making your software accessible to a broad range of users. In this tutorial, you will learn the main concepts of FastAPI and how to use it to quickly create web APIs that implement best practices by default.

By the end of it, you will be able to start creating production-ready web APIs, and you will have the understanding needed to go deeper and learn more for your specific use cases.

In this tutorial, you’ll learn how to:

  • Use path parameters to get a unique URL path per item
  • Receive JSON data in your requests using pydantic
  • Use API best practices, including validationserialization, and documentation
  • Continue learning about FastAPI for your use cases

This tutorial is written by the author of FastAPI. It contains a careful selection of fragments from the official documentation, avoiding getting lost in technical details while helping you get up to speed as fast as possible.

To get the most out of this tutorial, it would be helpful for you to know the basics of what HTTP is and how it workswhat JSON is, and Python type hints. You will also benefit from using a virtual environment, as is the case for any Python project.

What Is FastAPI?

FastAPI is a modern, high-performance web framework for building APIs with Python based on standard type hints. It has the following key features:

  • Fast to run: It offers very high performance, on par with NodeJS and Go, thanks to Starlette and pydantic.
  • Fast to code: It allows for significant increases in development speed.
  • Reduced number of bugs: It reduces the possibility for human-induced errors.
  • Intuitive: It offers great editor support, with completion everywhere and less time debugging.
  • Straightforward: It’s designed to be uncomplicated to use and learn, so you can spend less time reading documentation.
  • Short: It minimizes code duplication.
  • Robust: It provides production-ready code with automatic interactive documentation.
  • Standards-based: It’s based on the open standards for APIs, OpenAPI and JSON Schema.

The framework is designed to optimize your developer experience so that you can write simple code to build production-ready APIs with best practices by default.

Install FastAPI

As with any other Python project, it would be best to start by creating a virtual environment. If you are not familiar with how to do that, then you can check out the Primer on Virtual Environments.

The first step is to install FastAPI and Uvicorn using pip:

$ python -m pip install fastapi uvicorn[standard]

With that, you have FastAPI and Uvicorn installed and are ready to learn how to use them. FastAPI is the framework you’ll use to build your API, and Uvicorn is the server that will use the API you build to serve requests.

First Steps

To get started, in this section, you will create a minimal FastAPI app, run it with a server using Uvicorn, and then learn all the interacting parts. This will give you a very quick overview of how everything works.

Create a First API

A basic FastAPI file looks like this:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

Copy the code above to a file named main.py, and just like that, you have a fully functional API application with some best practices like automatic documentation and serialization built in. You will learn more about those features next.

This code defines your application, but it won’t run on itself if you call it with python directly. To run it, you need a server program. In the steps above, you already installed Uvicorn. That will be your server.

Run the First API App With Uvicorn

Run the live server using Uvicorn:

$ uvicorn main:app --reload

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [28720]
INFO:     Started server process [28722]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

The highlighted line in the output shows the URL where your app is being served in your local machine. Since you used --reload for development, when you update your application code, the server will reload automatically.

Check the Response

Open your browser to http://127.0.0.1:8000, which will make your browser send a request to your application. It will then send a JSON response with the following:

{"message": "Hello World"}

That JSON message is the same dictionary that you returned from the function in your application. FastAPI takes care of serializing the Python dict into a JSON object and setting the appropriate Content-Type.

Check the Interactive API Documentation

Now open http://127.0.0.1:8000/docs in your browser.

You will see the automatic interactive API documentation provided by Swagger UI:

Interactive API docs provided by Swagger UI

The web-based user interface documenting your API is provided and integrated by default. You don’t have to do anything else to take advantage of it with FastAPI.

Check the Alternative Interactive API Documentation

Now, go to http://127.0.0.1:8000/redoc in your browser.

You’ll see the alternative automatic documentation provided by ReDoc:

Interactive API docs provided by ReDoc

As FastAPI is based on standards like OpenAPI, there are many alternative ways to show the API documentation. FastAPI provides these two alternatives by default.

The First API, Step by Step

Now let’s analyze that code step by step and understand what each part does.

Step 1 is to import FastAPI:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

FastAPI is a Python class that provides all the functionality for your API.

Step 2 is to create a FastAPI instance:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

Here the app variable will be an instance of the class FastAPI. This will be the main point of interaction to create your API.

This app is the same one you referred to above in the command to run the live server with uvicorn :

$ uvicorn main:app --reload

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Before continuing with step 3, it’s worth taking a moment to get familiar with a couple of terms. Path refers to the last part of the URL starting from the first forward slash character (/). So, in a URL like https://example.com/items/foo, the path would be /items/foo.

A path is also commonly called an endpoint or a route, but the term path will be used in this tutorial. When you’re building an API, the path is the main way you can separate resources.

Another important term to know is operation, which is used in reference to any of the HTTP request methods:

  • POST
  • GET
  • PUT
  • DELETE
  • OPTIONS
  • HEAD
  • PATCH
  • TRACE

With HTTP, you can communicate with each path using one (or more) of these operations. Knowing what those two terms mean, you’re ready to continue with step three.

Step 3 is to define a path operation decorator:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

The @app.get("/") tells FastAPI that the function right below is in charge of handling requests that go to the path / using a get operation. This is a decorator related to a path operation, or a path operation decorator. If you want to learn a bit more about decorators, then check out the Primer on Python Decorators.

You can also use the other operations mentioned above:

  • @app.post()
  • @app.put()
  • @app.delete()
  • @app.options()
  • @app.head()
  • @app.patch()
  • @app.trace()

In each case, you would use the appropriate path operation decorator above a function that is in charge of handling those requests.

Step 4 is to define the path operation function, or the function that goes below the path operation decorator:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

This function will be called by FastAPI whenever it receives a request to the specified URL (/) using a GET operation. In this case, it is an async function.

You could also define it as a normal function instead of using async def:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return {"message": "Hello World"}

If you don’t know the difference between normal functions and async functions and when to use them, check out Concurrency and async/await in the FastAPI documentation.

Step 5 is to return the content:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

You can return a dictionarylist, or singular values as stringsintegers, and so on. You can also return pydantic models, which you’ll learn more about later.

There are many other objects and models that will be automatically converted to JSON, including object-relational mappers (ORMs) and others. Try using your favorite ones—it’s highly probable that they are already supported.

Path Parameters: Get an Item by ID

You can declare path parameters or variables with the same syntax used by Python formatted strings:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id):
    return {"item_id": item_id}

The value of the path parameter item_id will be passed to your function as the argument item_id.

So, if you run this example and go to http://127.0.0.1:8000/items/foo, you will see this response:

{"item_id":"foo"}

The response contains "foo", which is what was passed in the item_id path parameter and then returned in a dictionary.

Path Parameters With Types

You can declare the type of a path parameter in the function using standard Python type hints:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

In this case, you declare item_id to be an int.

Declaring the type of a path parameter will give you editor support inside of your function, with error checks, completion, and so on.

Data Conversion

If you run the above example and navigate your browser to http://127.0.0.1:8000/items/3, then you will see the following response:

{"item_id":3}

Notice that the value your function received and then returned is 3, which is a Python int, not a string ("3"). So, with that type declaration, FastAPI gives you automatic request parsing.

Data Validation

If you point your browser to http://127.0.0.1:8000/items/foo, then you’ll see a nice HTTP error:

{
    "detail": [
        {
            "loc": [
                "path",
                "item_id"
            ],
            "msg": "value is not a valid integer",
            "type": "type_error.integer"
        }
    ]
}

This is because the path parameter item_id has a value of "foo", which is not an int.

The same error would appear if you provided a float instead of an int, such as if you opened http://127.0.0.1:8000/items/4.2 in your browser. So, with the same Python type hint, FastAPI gives you both data parsing and data validation.

Also notice that the error clearly states the exact point where the validation didn’t pass. This is incredibly helpful while developing and debugging code that interacts with your API.

Documentation

When you open your browser at http://127.0.0.1:8000/docs, you will see an automatic, interactive API documentation:

Interactive API docs for path operations

Again, with that same Python type declaration, FastAPI gives you automatic, interactive documentation integrating Swagger UI. Notice that the path parameter is declared to be an integer.

Because FastAPI is built on top of the OpenAPI standard, it also provides an alternative API documentation using ReDoc, which you can access at http://127.0.0.1:8000/redoc:

Alternative interactive API docs using ReDoc

There are many other compatible tools, including code-generation tools for many languages.

Data Handling With pydantic

All the data validation is performed under the hood by pydantic, so you get all the benefits from it, and you know you are in good hands.

You can use the same type declarations with strfloatbool and many other complex data types.

Order Matters: Put Fixed Paths First

When creating path operations, you may find situations where you have a fixed path, like /users/me. Let’s say that it’s to get data about the current user. You might also have the path /users/{user_id} to get data about a specific user by some user ID.

Because path operations are evaluated in order, you need to make sure that the path for /users/me is declared before the one for /users/{user_id}:

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}

@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {"user_id": user_id}

Otherwise, the path for /users/{user_id} would also match for /users/me, thinking that it’s receiving the parameter user_id with a value of "me".

Request Body: Receiving JSON Data

When you need to send data from a client to your API, you send it as a request body.

request body is data sent by the client to your API. A response body is the data your API sends to the client. Your API almost always has to send a response body. But clients don’t necessarily need to send request bodies all the time.

To declare a request body, you use pydantic models, with all their power and benefits. You’ll learn more about them below.

Use pydantic to Declare JSON Data Models (Data Shapes)

First, you need to import BaseModel from pydantic and then use it to create subclasses defining the schema, or data shapes, you want to receive.

Next, you declare your data model as a class that inherits from BaseModel, using standard Python types for all the attributes:

# main.py

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    return item

When a model attribute has a default value, it is not required. Otherwise, it is required. To make an attribute optional, you can use None.

For example, the model above declares a JSON object (or Python dict) like this:

{
    "name": "Foo",
    "description": "An optional description",
    "price": 45.2,
    "tax": 3.5
}

In this case, since description and tax are optional because they have a default value of None, this JSON object would also be valid:

{
    "name": "Foo",
    "price": 45.2
}

A JSON object that omits the default values is also valid.

Next, add the new pydantic model to your path operation as a parameter. You declare it the same way you declared path parameters:

# main.py

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    return item

The parameter item has a type hint of Item, which means that item is declared as an instance of the class Item.

With that Python type declaration, FastAPI will:

  • Read the body of the request as JSON
  • Convert the corresponding types if needed
  • Validate the data and return a clear error if it is invalid
  • Give you the received data in the parameter item—since you declared it to be of type Item, you will also have all the editor support, with completion and type checks for all the attributes and their types
  • Generate JSON Schema definitions for your model that you can also use anywhere else that makes sense for your project

By using standard type hints with pydantic, FastAPI helps you build APIs that have all these best practices by default, with little effort.

Automatic Documentation With pydantic

The JSON Schemas of your pydantic models will be part of the OpenAPI generated for your application and will be shown in the interactive API documentation:

JSON Schema for pydantic models is included in API docs UI

You can see that the attributes of Item in the API documentation are exactly the ones you declared with your pydantic model.

These JSON Schemas will also be used in the API documentation inside each path operation that needs them:

JSON Schema from pydantic models is included in each path operation in the API docs UI

Notice that all this automatic documentation is based on your data, using your pydantic models.

Editor Support, Autocompletion, and Type Checks

In your editor, inside your function, you will get type hints and completion everywhere. This wouldn’t happen if you received a dict instead of a pydantic model:

Editors provide completion and type checks for pydantic models

This way you can trigger autocompletion for all your data.

You also get error checks for incorrect type operations:

Editors provide error checks for pydantic models

In this case, you cannot sum a str with a float, and as the editor knows those types, it can warn you that you have an error in your code. This is not by chance: The whole framework was built around that design. It was thoroughly tested at the design phase, before any implementation, to ensure it would work with all the editors. There were even some changes to pydantic itself to support this feature.

The previous screenshots were taken with Visual Studio Code. But you would get the same editor support with PyCharm and most of the other Python editors:

PyCharm also provides editor support like VS Code

If you use PyCharm as your editor, then you can use the pydantic PyCharm plugin to improve your editor support. And if you use VS Code, then you will get the best developer experience with Pylance.

Use the pydantic Model

Inside the function, you can access all the attributes of the model object directly:

# main.py

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    item_dict = item.dict()
    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
    return item_dict

The parameter item is declared as an instance of the class Item, and FastAPI will make sure that you receive exactly that in your function instead of a dictionary or something else.

Request Body and Path Parameters

You can declare path parameters and a request body at the same time.

FastAPI will recognize that the function parameters that match path parameters should be taken from the path and that function parameters that are declared to be pydantic models should be taken from the request body:

# main.py

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

app = FastAPI()

@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item):
    return {"item_id": item_id, **item.dict()}

This way, you can declare path parameters and JSON request bodies, and FastAPI will take care of doing all the data validation, serialization, and documentation for you. You could verify it by going to the same API documentation at /docs or by using other tools like Postman with a graphical interface or Curl in the command line.

In a similar way, you can declare more complex request bodies, like lists, and other types of request data, like query parameters, cookies, headers, form inputs, files, and so on.

Learn More About FastAPI

By this point, you already know a lot about FastAPI and how to use it to create robust and production-ready APIs.

But there’s a lot more that you could learn:

FastAPI can cover most of the use cases required for back-end frameworks, even the ones that are not strictly APIs. You can dive deeper into the documentation to solve your specific use case.

FastAPI is based on modern Python features, and you can take full advantage of FastAPI by learning more about those features, too. Check out Getting Started With Async Features in Python and Async IO in Python: A Complete Walkthrough to learn more about asyncronous programming. You can also check out Python Type Checking (Guide) to get all the traditional benefits from type hints in your code.

Conclusion

In this tutorial, you learned about FastAPI and how to use it to create production-ready APIs that have best practices by default while providing the best developer experience possible. You learned how to:

  • Use path parameters to get a unique URL path per item
  • Receive JSON data in your requests using pydantic
  • Use API best practices like validationserialization, and documentation
  • Continue learning about FastAPI and pydantic for different use cases

You’re now ready to start creating your own highly performant APIs for your projects. If you want to dive deeper into the world of FastAPI, then you can follow the official User Guide in the FastAPI documentation.

Comments

Popular posts from this blog

Flutter for Single-Page Scrollable Websites with Navigator 2.0

A Data Science Portfolio is More Valuable than a Resume

Better File Storage in Oracle Cloud