An Introduction to ⚡FastAPI

An Introduction to ⚡FastAPI

refine repo

Author: Obisike Treause

Introduction

Since its introduction to backend development, Python has grown in popularity, competing with pre-existing heavyweights such as PHP and .Net. It has made the developer experience more efficient and streamlined by introducing simplicity and power. Despite being known to be slower than its counterpart, Python has thrived greatly in this ecosystem.

Several frameworks for developing web APIs have been developed, such as Django and Flask, but the underlying speed problem has always been present. As a result, another Python framework, FastAPI, has been developed to combat this issue.

Steps we'll cover:

What is FastAPI

FastAPI is a modern Python microframework that simplifies the creation of web APIs using Python programming. It allows developers to swiftly and easily build APIs, ensuring optimal performance and easy management without compromising code quality and efficiency.

It provides numerous advantages, including exceptional speed, outperforming several other Python backend frameworks, and competing with popular frameworks like Express.js.

FastAPI offers the simplicity of Flask as it closely resembles Flask but still packs out-of-the-box configurations such as validation, documentation, and response encoding.

Benefits of using FastAPI

As previously emphasized, FastAPI stands out due to its exceptional benefits and extensive advantages. Let's delve into some of the notable benefits it offers:

  • Performance: FastAPI maximizes performance by utilizing the full potential of critical libraries and tools such as Pydantic and the ASGI ecosystem. Furthermore, because of its solid foundation on the Starlette framework, it seamlessly integrates the power of async/await functionality.

  • Scalability: The modularity and simplicity of FastAPI allow for seamless integration with load balancers, facilitating scalability and ensuring efficient resource utilization.

  • Automatic Documentation: By requiring the explicit definition of various FastAPI components, Pydantic's integration has allowed FastAPI to be able to generate its API documentation automatically. FastAPI provides Swagger API documentation.

  • Ease-of-use: FastAPI is a Python framework, so the benefits of using Python are inherited. Not only that, FastAPI makes creating your server and building endpoints simple and quick.

  • Request Validation: FastAPI provides request validation with a much more detailed error message readable by users. This is also attributed to its use of Pydantic for request data type specification.

Comparing FastAPI with other Python frameworks

FastAPI, a relatively new addition to the backend API ecosystem, competes with established Python giants such as Flask and Django. While Flask and Django have been recognized as leading frameworks in this space, it's important to see how they compare to FastAPI.

Django Vs FastAPI

Django is a feature-rich Python backend framework that includes a variety of built-in libraries to meet the needs of various projects. It has powerful features like ORM, authentication mechanisms, and routing capabilities, which make it suitable for developing complex web applications.

FastAPI, on the other hand, shines as a nimble microframework that was purposefully designed to be lightweight. While it lacks a large library ecosystem, it compensates by being extremely fast. Unlike Django, which is limited by its app system, FastAPI uses modern Python techniques to unlock its inherent advantages and improve its performance.

Flask Vs FastAPI

Flask is a lightweight framework used by Python developers to quickly build web applications. Flask stands out for its design, giving developers more control and flexibility when structuring their applications, ensuring that the applications are tailored to their specific needs or requirements.

FastAPI, on the other hand, focuses on developing highly performant and scalable applications with exceptional speed. It has additional benefits discussed in this article and is best suited for complex applications.

Pyramid Vs FastAPI

Pyramid is yet another intriguing flavor of one of Python's most popular backend frameworks. It adheres to the "use only what you need" philosophy, which means it provides a minimalistic core that can be supplemented with various add-ons and libraries. This modular approach enables developers to pick and choose which components they need for their specific use cases, resulting in a lightweight and highly customizable framework.

FastAPI, on the other hand, prioritizes developer productivity and ease of use. It has a simple and intuitive API design, as well as clear documentation and extensive examples. It also comes with some pre-built tools, such as auto-documentation generation.

Getting Started

With FastAPI, you can easily set up a project in a few steps. Firstly, like any other Python project, you'll need to set up your virtual environment. After that, you’ll need to install the packages, FastAPI and Uvicorn.

To do this, run the command:

python -m pip install fastapi 'uvicorn[standard]'

With that, you can start creating the endpoints required for your application.

The packages fastapi and uvicorn are essential for setting up a FastAPI project. The package uvicorn creates the server that runs the FastAPI setup, while fastapi provides the necessary methods and configurations for creating endpoints.

Creating your first route

To create your first route, create a file, main.py, which will contain all your code. Open the file in your text editor and add the following:

from fastapi import FastAPI

fastapi = FastAPI()

@fastapi.get("/")
async def home():
    return {"data": "Hello World"}

In the above snippet, you’ll notice that you are importing the FastAPI class from the fastapi module and then instantiating the class.

The instance of the FastAPI class can then be used as a decorator for the handler function to set up the endpoints. This instance provides the REST API verbs such as PUT, DELETE, PATCH, GET, and POST and a way to set the resource path.

Once that is done, you can run the following command on the terminal to start the server.

uvicorn main:fastapi --reload

The main represents the module import, and the fastapi is an instance of the FastAPI. The command above starts the server, which can then be accessed via the browser at http://127.0.0.1:8000.

introduction to fast api

To add another route, simply create a handler function e.g.

def handler():
    return { "data": "from handler"}

After that, using the FastAPI instance you created, add the decorator

@fastapi.post("/home-page")

To the function. This converts the function to an API endpoint.

You can use @fastapi.post, @fastapi.put or @fastapi.patch or @fastapi.delete to create different endpoints.

Managing request and response bodies using FastAPI models

Sending and receiving data to an endpoint is a fundamental aspect of API development. When it comes to the sending aspect, there are multiple methods for sending data to an endpoint, one of which includes the following:

Path parameters: This method involves attaching short data directly to the URL path. To implement this functionality in a FastAPI's endpoint, you can refer to the example below:

# other data goes here
@fastapi.get("/{name}")
async def get_name(name: str):
    return { "name": name }

In the above example, the name is a path parameter that is extracted from the URL and passed as a parameter to the get_name function. This way, you can conveniently access the data sent through the URL path.

Query parameters: They are similar to the path parameter, but the difference is that the query parameters are appended to the URL after a question mark ("?"). To implement query parameters in a FastAPI's endpoint, you can refer to the example below:

@fastapi.get("/")
async def get_api_data(data_type: str, skip: int = 0, limit: int = 10):
    return { "data_type": data_type, "skip": skip, "limit": limit }

In the above example, the skip and limit parameters are query parameters. They are provided via the URL after the endpoint ("/") using the format ?key=value. The skip and limit parameters have default values of 0 and 10, respectively, but can be overridden by providing different values in the URL.

Body parameters: This type is different from the previously mentioned. It’s a method whereby the data is encoded and appended to the request made to the endpoint. The body parameter is implemented in the following way:

from pydantic import BaseModel
from fastapi import FastAPI

fastapi = FastAPI()
class Item(BaseModel):
    name: str
    price: float

@fastapi.post("/items")
async def create_item(item: Item):
    return {"item": item}

In the above example, we define an Item class that inherits from BaseModel provided by Pydantic. This system defines the whole request body parameters and provides more context to FastAPI.

Headers and Cookies: One of the major ways of sending extra information for context to the server is through headers and cookies. To do this with FastAPI, simply follow the example below.
For Headers:

from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/items")
async def read_items(user_agent: str = Header(None)):
    return {"User-Agent": user_agent}

In the above example, the user_agent parameter is given the default, Header(None), which tells FastAPI to extract the value of the header from the request. If the header is present in the request, user_agent will be set to it; otherwise, user_agent will be set to None.

For Cookies:

from fastapi import FastAPI, Cookie

app = FastAPI()

@app.get("/items/")
async def read_items(session_token: str = Cookie(None)):
    return {"session_token": session_token}

In this example, similarly to headers, the session_token parameter is given the default value of Cookie(None), indicating that FastAPI should inject the cookies set on the request into the session_token parameter. If no cookies are set, then the value of the session_token will be None.

Preview the API Documentation

Once the server is running on the browser, visit 127.0.0.1:8000/docs

introduction to fast api

Understanding FastAPI by building a REST API for an inventory application

To demonstrate the power of FastAPI, you’ll be building a REST API for a hypothetical inventory application. This API will be connected to a database, support image uploads, and have protected routes. This API will have the following endpoints:

  • GET /items - To fetch all the items stored on the server

  • GET /items/{item_id} - to get a specific item from the server

  • POST /items - to get add a new item to our server

  • PATCH /items/{item_id} - to update the item on the server

  • DELETE /items/{item_id} - to delete an item from the inventory

Then you’ll be creating more endpoints to handle the serving of files, and you'll also be adding a database and some sort of authentication to some of the endpoints using a middleware.

Prerequisites

To follow along, you’ll need to have the following:

  • Knowledge of Python

  • Knowledge of HTTP, JSON, REST API and Python’s Virtual Environment

  • A terminal

  • Python 3.10 installed

Setting the project

To do this, set up a virtual environment by following the instructions. Once done, you must create the src directory in the virtual environment folder. This is where your code will reside.

Open the directory using your code editor.

introduction to fast api

Installing Dependencies

You'll only need essential dependencies such as Uvicorn and FastAPI to accomplish this. However, since you'll be enhancing the server's capabilities by incorporating a database connection and a file upload system, installing additional dependencies like databases and SQLAlchemy is necessary.

To proceed, run the command:

pip install 'fastapi[all]' 'uvicorn[standard]' databases sqlalchemy

This will install all the necessary dependencies for this project.

Creating Your Endpoints

In your src directory, create a new file, main.py; this will be the entry point of your application.

So on the main.py, add the following:

from fastapi import FastAPI
from fastapi import HTTPException
from .utils import find_item
from pydantic import BaseModel, Field
from typing import Optional

fastapi = FastAPI()

inventory = [
  { "id": 1, "name": "Treasure", "quantity": 3 }
]


class Item(BaseModel):
  name: str
  quantity: int

class ItemUpdate(BaseModel):
  name: Optional[str] = Field(None, description="Optional name of the item")
  quantity: Optional[int] = Field(None, description="Optional quantity of the item")

@fastapi.get("/items") 
async def get_items():
  return {"items": inventory}


@fastapi.get("/items/{item_id}")
async def get_item(item_id: int):
  item, idx = find_item(inventory, lambda x: x["id"] == item_id)
  return { "item": item }

@fastapi.delete("/items/{item_id}")
async def delete_item(item_id: int, authenticated: bool = Depends(authenticate)):
  item, idx = find_item(inventory, lambda x: x["id"] == item_id)
  if idx == -1: return HTTPException(404, "item not found")
  inventory.pop(idx)
  return { "item": item }



@fastapi.post("/items")
async def add_item(data: Item):
  item = {
    "id": len(inventory) + 1,
    "name": data.name,
    "quantity": data.quantity
  }
  inventory.append(item)
  return item

@fastapi.patch("/items/{item_id}")
async def update_item(item_id: int, item_update: ItemUpdate):
  item, idx = find_item(inventory, lambda x: x["id"] == item_id)
  if idx == -1:
    raise HTTPException(status_code=404, detail="Item not found")

  if item_update.name is not None:
    item["name"] = item_update.name

  if item_update.quantity is not None:
    item["quantity"] = item_update.quantity

  inventory[idx] = item

  return item

In the snippet above, there are five handlers for the endpoints. You have a local inventory data store that saves all the added items. The ItemUpdate class specifies the body parameter for the patch endpoint, allowing for optional parameters. The Optional class from the typing module and the Field class from pydantic are imported for creating these optional fields.

Add the utility functions to a file, utils.py, in the src directory.

Following that, you can run the server to test the endpoints via the documentation.

Advanced Concepts in FastAPI

APIs are usually not basic, like the inventory API created. Sometimes, you'll need to persist data or validate credentials before performing requests or handling files.

For your inventory app, you'll be adding the validation middleware, the file upload, and the database.

Implementing the Authentication Middleware

Middlewares are like the valves of the API world. They can be used to do a lot of things, like restricting access to certain users, adding extra context to the request, and many more. To demonstrate its capability, you are going to create a middleware that allows access to clients that have a certain credential and add it to some of the endpoints of the inventory app.

Create a new file in the src directory called middleware.py and add the following code:

from fastapi import HTTPException, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials

security = HTTPBasic()

def authenticate(credentials: HTTPBasicCredentials = Depends(security)):
    correct_username = 'admin'
    correct_password = 'password'
    if credentials.username != correct_username or credentials.password != correct_password:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return True

In the case above, the middleware, authenticate, implements basic authentication. To add this to your desired route handler, go to the main.py file, locate the endpoints you want to add authentication and add the parameter:

authenticated: bool = Depends(authenticate)

After that, the handler will look like this:

@fastapi.post("/items")
async def add_item(data: Item, authenticated: bool = Depends(authenticate)):
  item = {
    "name": data.name,
    "id": len(inventory) + 1,
    "quantity": data.quantity
  }
  inventory.append(item)
  return item

Ensure you import the authenticate from the middleware.py you created and Depends from fastapi.

Database Integration

To add a database to our existing inventory app, we must first ensure that sqlalchemy and databases are installed. Then create a file, “database.py” in the src directory, Then we need to import the following:

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

You’ll be using SQLite due to its simplicity.

Next, you'll need to create the database engine by adding the code to the file

DATABASE_URL = "sqlite:///./database.db"
engine = create_engine(DATABASE_URL)

This is the basic connection configuration for the database. After that, create the SessionLocal class, which creates a database session when called, and the Base class, which will serve as the BaseModel for all the database models to inherit.

On the same database.py file, add the following:

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

Next, create a database model by creating by adding the following:

class DBItem(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    quantity = Column(Integer)
    name = Column(String)

After that, you'll need to import the DBItem and the SessionLocal from the database.py file to the main.py file.

Additionally, make sure to update the route handlers accordingly.

@fastapi.post("/items")
def create_item(item: Item, authenticated: bool = Depends(authenticate)):
    db = SessionLocal()
    new_item = DBItem(name=item.name, quantity=item.quantity)
    db.add(new_item)
    db.commit()
    db.refresh(new_item)
    return {"item" :new_item}

@fastapi.get("/items")
async def get_items():
  db = SessionLocal()
  items = db.query(DBItem).all()
  return { "items": items }


@fastapi.get("/items/{item_id}")
def get_item(item_id: int):
    db = SessionLocal()
    item = db.query(DBItem).filter(DBItem.id == item_id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": item}

@fastapi.patch("/items/{item_id}")
def update_item(item_id: int, item: ItemUpdate, authenticated: bool = Depends(authenticate)):
    db = SessionLocal()
    db_item = db.query(DBItem).filter(DBItem.id == item_id).first()
    if not db_item:
        raise HTTPException(status_code=404, detail="Item not found")
    db_item.name = item.name
    db_item.quantity = item.quantity
    db.commit()
    db.refresh(db_item)
    return { "item": db_item}

@fastapi.delete("/items/{item_id}")
def delete_item(item_id: int, authenticated: bool = Depends(authenticate)):
    db = SessionLocal()
    db_item = db.query(DBItem).filter(DBItem.id == item_id).first()
    if not db_item:
        raise HTTPException(status_code=404, detail="Item not found")
    db.delete(db_item)
    db.commit()
    return {"message": "Item deleted"}

In the code above, you’ll notice that you are instantiating the SessionLocal for each endpoint, which is then used to perform the database queries. After the query is performed, you can then commit it to the database for persistence.

File Uploads

With FastAPI, file uploads can be done very easily. We’ll use our existing API to demonstrate how file uploads and serving can be achieved. First, you’ll have to update the DBItem in your database.py file by adding the following:

class DBItem(Base):
    # other database fields goes here
    image_src = Column(String)

After that, on the main.py, import File and UploadFile from fastapi, import os and then import FileResponse from starlette.responses. After that, add the following:

@fastapi.patch("/item-image/{item_id}")
async def upload_file(item_id: int, file: UploadFile = File()):
    db = SessionLocal()
    db_item = db.query(DBItem).filter(DBItem.id == item_id).first()
    if not db_item:
        raise HTTPException(status_code=404, detail="Item not found")

    file_path = os.path.join("uploads", file.filename)
    with open(file_path, "wb") as f:
        f.write(await file.read())

    db_item.image_src = file.filename
    db.commit()
    db.refresh(db_item)

    return {"item": db_item}

@fastapi.get("/static/{file}")
async def serve_file(file: str):
    return FileResponse(os.path.join("uploads", file))

The first route handler in the snippet above updates the image_src field of the specified item and uploads the file to the server. Then the second, serve_file, handles the file retrieval from the server.

Starlette offers several other types of responses, including the FileResponse, which can then be used on the route handler.

introduction to fast api

The source code for this inventory API can be found here.

Conclusion

Congratulations on making it this far! By now, you should have gained valuable insight into using FastAPI to build your backend application. FastAPI is an exceptional, user-friendly, and highly effective API development tool. It provides the flexibility associated with a microframework and delivers exceptional performance, making it an excellent choice for your API development requirements.

Resources