Post

Lesson 9 - Extra - Intro to FastAPI

9.1. Intro

FastAPI

9.1.1. Some online resources

9.1.2. What is FastAPI?

  • FastAPI is a modern, high-performance Python web framework for building APIs quickly and efficiently. It is built on top of Starlette for web routing and ASGI support, and Pydantic for data validation and parsing.
  • FastAPI is designed to be easy to use while offering great performance. It supports asynchronous programming, making it suitable for high-concurrency applications.
  • FastAPI automatically generates interactive API documentation in two formats: ReDoc and OpenAPI/Swagger, allowing developers and clients to easily explore and test API endpoints.
  • Although it does not have built-in support for specific databases like MongoDB or Redis, FastAPI works seamlessly with any database through third-party libraries, including SQL databases (via SQLAlchemy) and NoSQL databases (such as MongoDB).
  • It is based on Python 3.7+ and utilizes Python’s type hints to offer automatic validation, serialization, and documentation.

Key Features:

  • High Performance: Comparable to frameworks like Node.js and Go.
  • Automatic Documentation: FastAPI automatically generates interactive API docs using Swagger UI and ReDoc.
  • Asynchronous Support: FastAPI supports asynchronous request handling via async and await.
  • Data Validation: Built on Pydantic, making request validation and parsing easy.
  • Type Hints: Leverages Python’s type hints for better code completion and editor support.

9.2. Setting Up FastAPI

As always, we first need to setup the environment. In a new directory.

1
python3 -m venv venv

9.2.1. Installation

Let us then install the necessary dependencies with the following command. We should also add them to the requirements.txt file.

1
2
pip install fastapi uvicorn
pip freeze > requirements.txt
  • fastapi: The main FastAPI framework.
  • uvicorn: ASGI server to run the FastAPI app.

this is the basic folder structure for this lesson

1
2
3
.
├── app.py
└── requirements.txt

9.2.2. Basic FastAPI App

Let us make a basic FastAPI app that will return the message “Hello World!”.

1
2
3
4
5
6
7
from fastapi import FastAPI

app = FastAPI()

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

9.2.3. Run the app

We are going to use uvicorn to run the FastAPI app. This will start the server and listen for requests on port 8000.

1
uvicorn app:app --reload --port 8000

You can also ommit --port 8000, because the default port is 8000.

We will perform a simple test with the browser.

Open your browser at http://localhost:8000. The app should be running. We should have a JSON response displated:

1
{"message": "Hello World!"}

9.2.4. Creating Basic API Routes (GET, POST, PUT, DELETE)

Let’s explore the basic HTTP methods: GET, POST, PUT, and DELETE. With these we are following the so called CRUD pattern, that is:

ActionMethodDescription
CreatePOSTCreate a new item.
ReadGETGet an item.
UpdatePUTUpdate an item.
DeleteDELETEDelete an item.

First, let’s add the pydantic import and the basic Item model. Item will be our data model type.

1
2
3
4
5
6
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str = None
    price: float

GET Method

  • We can get an item using the GET method. In this case we will use the item_id as the parameter to get a specific item.
1
2
3
@app.get("/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id}

If we wish to get all items we can use the GET method without any parameters.

1
2
3
@app.get("/items/")
def read_items():
    return [{"item_id": "Foo"}, {"item_id": "Bar"}]

POST Method

1
2
3
@app.post("/items/")
def create_item(item: Item):
    return {"item_name": item.name, "item_price": item.price}

PUT Method

1
2
3
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_id": item_id, "updated_item": item}

DELETE Method

1
2
3
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
    return {"message": f"Item {item_id} deleted."}

9.2.5. Request and Response Models

FastAPI automatically handles request validation using Pydantic models. These models can also be used to define the response structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
from fastapi import FastAPI
from pydantic import BaseModel

class User(BaseModel):
    username: str
    email: str
    full_name: str = None

app = FastAPI()

@app.post("/users/")
def create_user(user: User):
    return {"username": user.username, "email": user.email}

This will automatically validate the incoming request body to ensure it matches the model, rejecting requests with invalid data.

9.2.6. Dependency Injection

FastAPI provides a way to handle dependencies, such as database connections, authentication, and logging, using dependency injection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from fastapi import Depends, FastAPI

app = FastAPI()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
    items = db.query(Item).offset(skip).limit(limit).all()
    return items

In this example, get_db is a dependency that can be injected into route handlers using the Depends function. FastAPI will ensure that the dependency is initialized and cleaned up automatically.

9.2.7. Automatic Documentation

FastAPI automatically generates interactive API documentation. You can access the documentation via:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc

These tools allow you to test API endpoints directly from the browser.

9.2.8. Handling Errors

FastAPI makes it easy to handle errors and send appropriate HTTP responses.

1
2
3
4
5
6
7
from fastapi import HTTPException

@app.get("/items/{item_id}")
def read_item(item_id: int):
    if item_id not in [1, 2, 3]:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item_id": item_id}

If you try to access an invalid item_id, FastAPI will return a 404 error with the custom message “Item not found”.

9.2.9. Middleware

You can also add middleware to perform actions on every request and response, like logging or adding custom headers. Middleware is a function that runs before or after the request is processed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware

app = FastAPI()

class SimpleMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        response.headers["X-Custom-Header"] = "Custom value"
        return response

app.add_middleware(SimpleMiddleware)

@app.get("/")
def read_root():
    return {"message": "Hello with middleware!"}

9.3. An Example App with Database

In this example, we will create an app that allows users to create, read, update, and delete items in a database.

9.3.1. Create Environment and Install Dependencies

1
2
3
python3 -m venv venv
pip install pip install fastapi uvicorn sqlalchemy sqlite3
pip freeze > requirements.txt

9.3.2. Setting Up SQLite with FastAPI and SQLAlchemy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session

# Define the SQLite database URL
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

# Create the SQLite engine
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})

# Create a session local class for handling database sessions
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Create a base class for models
Base = declarative_base()

# Create the FastAPI app
app = FastAPI()

# Define a database model
class Item(Base):
    __tablename__ = "items"

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

# Create the database tables
Base.metadata.create_all(bind=engine)

# Dependency to get the database session
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

9.3.3. CRUD Functionality Implementation

  • Create Item

We will use the POST method to create a new item in the database.

1
2
3
4
5
6
7
@app.post("/items/")
def create_item(name: str, description: str, db: Session = Depends(get_db)):
    new_item = Item(name=name, description=description)
    db.add(new_item)
    db.commit()
    db.refresh(new_item)
    return new_item
  • Read Items (with pagination)

The GET method will retrieve all items from the database with support for pagination.

1
2
3
4
@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
    items = db.query(Item).offset(skip).limit(limit).all()
    return items
  • Read Single Item by ID

We will retrieve an item by its ID using the GET method.

1
2
3
4
5
6
@app.get("/items/{item_id}")
def read_item(item_id: int, db: Session = Depends(get_db)):
    item = db.query(Item).filter(Item.id == item_id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return item
  • Update Item

We can update an existing item using the PUT method.

1
2
3
4
5
6
7
8
9
10
11
@app.put("/items/{item_id}")
def update_item(item_id: int, name: str, description: str, db: Session = Depends(get_db)):
    item = db.query(Item).filter(Item.id == item_id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    
    item.name = name
    item.description = description
    db.commit()
    db.refresh(item)
    return item
  • Delete Item

To delete an item by its ID, we use the DELETE method.

1
2
3
4
5
6
7
8
9
@app.delete("/items/{item_id}")
def delete_item(item_id: int, db: Session = Depends(get_db)):
    item = db.query(Item).filter(Item.id == item_id).first()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    
    db.delete(item)
    db.commit()
    return {"message": f"Item {item_id} deleted"}

9.3.4. Running the App

To run the FastAPI application, use the following command:

1
uvicorn app:app --reload

This will start the application at http://127.0.0.1:8000/ or http://localhost:8000/, and you can interact with the API using the automatically generated documentation at http://127.0.0.1:8000/docs or http://localhost:8000/docs.

This post is licensed under CC BY 4.0 by the author.