Introduction

I’ve been hearing a lot about a Python web framework called FastAPI. The purpose of this post is to understand the basics of FastAPI by exploring:

  • What is FastAPI?
  • When to use it?
  • When not to use it?
  • FastAPI vs DRF
  • FastAPI Architecture
  • FastAPI Core Components
  • How to get started with a simple project

What is FastAPI?

FastAPI is a Python web framework for building APIs. Decorators (like @app.get(“/end_point”) are used to define endpoints, it uses Python type hints to automatically validate incoming data, convert types and generate interactive API documentation (at /docs) out of the box.

It is built on Starlette (for the web layer) and Pydantic (for data validation)

What is Starlette?

Starlette is a lightweight python web framework built on ASGI (Asynchronous Server Gateway Interface) which is sucessor to WSGI (Web Server Gateway Interface). FastAPI is built on top of Starlette by adding automatic data validation, serialization and API docs to it.

What is Pydantic?

Pydantic is a python library for data validation using type hints. Let me elaborate with an illustration for better understanding.

Pydantic model example

from pydantic import BaseModel

class Customer(BaseModel):
    name: str
    age: int
    email: str

user = Customer(name="Alice", age=25, email="alice@example.com")  # works
user = Customer(name="Alice", age="not a number", email="...")    # validation error

The above class takes 3 parameters as input name as string, age an integer, and email as string. If the user passes an invalid data type like e.g., age=”not a number” it will throw a validation error as it is expecting an integer. However, Pydantic does “type coercion”. Let us consider the user passes age=”45”, the expectation is to throw a validation error as the input is a “string” and expected type is “int” but in reality due to the intervention of “type coercion” the string is automatically converted to integer and the execution continues.

FastAPI uses Pydantic under the hood so that when a request comes in, the JSON body is automatically parsed and validated against your model before your function even runs.

When to choose FastAPI?

One should use FastAPI if speed, validation and clean code take precedence. The best use cases would be:

  • Building highly performant REST APIs
  • ML/AI services
  • Auto-generated documentation matters
  • Rapid prototyping

When not to choose FastAPI?

If your ecosystem is already using a robust framework (like Django for example) it is not advisable to switch to FastAPI unless you have a niche requirement to do so. Some factors to help make the decision are listed below:

  • Avoid FastAPI if your application involves CPU-Intensive tasks (image processing,video encoding etc). You can workaround it but it is not possible out of the box.
  • If you need one web framework with Full Stack needs then FastAPI is not the right fit.
  • Asynchronous programming comes with a little bit of learning curve, if the team has less experience in asynchronous programming and do not have the luxury of investing time to learn it then it is advisable not to use FastAPI.
  • If your project does not support async or does not have the need of async then it is best to avoid FastAPI. Without async you will lose the primary performance benefit of FastAPI. You can still use FastAPI without async but there are other frameworks better suited for the use case.

FastAPI vs DRF

Feature FastAPI DRF
Based on Starlette+Pydantic Django
Performance Very fast Slower
Object Relational Mapping Not built-in Django ORM (built-in and powerful)
Admin Panel No Yes (Django admin)
Authentication & Permissions Basic Rich & built-in)

Core Components

fastAPI_Architecture

  • ASGI Server (Uvicorn) The server that runs the FastAPI app. It receives HTTP requests from clients and sends responses back. Built on uvloop (a fast async event loop) and httptools (a fast HTTP parser)

  • Starlette FastAPI directly subclasses Starlette, inheriting all its capabilities. It handles the routing, middleware, request/response objects, websockets and static files.

  • Pydantic This component is responsible for data validation. It powers all input validation and output serialization. Once you define a BaseModel using Python type hints, FastAPI enforces it automatically — no extra code needed for it.

  • OpenAPI & Auto Docs FastAPI auto-generates an OpenAPI schema from your code. Two interactive docs UIs are available out of the box — Swagger UI at /docs and ReDoc at /redoc - without any extra setup

  • Dependency Injection (DI) System A clean system to share reusable logic across routes — database connections, authentication, configuration. FastAPI resolves dependencies automatically before calling your handler.

    Dependency Injection example?

    from fastapi import FastAPI, Depends
    
    app = FastAPI()
    
    # --- Dependency function ---
    def get_db():
      db = connect_to_database()
      try:
          yield db        # provides the db to the route
      finally:
          db.close()      # runs after the route finishes
    
    # --- Route using the dependency ---
    @app.get("/items")
    def read_items(db = **Depends**(get_db)):
      return db.query("SELECT * FROM items")
    

    Steps:

    1. Define a dependency function (for e.g., def get_db which returns a database handle)
    2. Call the dependency function name in the route using “Depends” (Depends(get_db)

    Dependency Injection vs Direct Call

    Looking at the above example, one may argue that one can use the direct call of the method (e.g., get_db()) why use dependency injection? On the surface, it looks similar but it does more than a direct call. Below are some points that make it different:

    1. LifeCycle Management With a direct call, you have to manage setup and teardown yourself:
      python# Direct — you manage everything
      @app.get("/items")
      def read_items():
         db = get_db()
         try:
             result = db.query(...)
             return result
         finally:
             db.close()   # easy to forget!
      

      With Depends(), the dependency handles it:

      def get_db():
         db = connect()
         try:
             yield db
         finally:
             db.close()   # always runs, even on errors
      
      @app.get("/items")
      def read_items(db = Depends(get_db)):
         return db.query(...)   # clean, nothing to manage
      
    2. Caching within a request If multiple routes need the same method, Depends() calls it only once per request and reuses the result but with direct call the method is called multiple times within the same request.

    3. Automatic parameter injection Dependencies can read query params, headers, and path params directly from the request — without you passing anything but with a direct call, you’d have to extract the header manually in every route.
  • Middleware Functions that run on every request before it reaches your route handler, and on every response before it goes back to the client. Common uses: CORS, logging, authentication, GZip compression.

Fast API Workflow

fastAPI_Workflow

Getting Started

Prerequisites

  • Python 3.10 or higher

Problem Statement

Develop APIs for a book store. The API should be able to list all books in stock, add a new book to the bookstore inventory, update a book entry and delete a book entry

1. Install fastapi module

pip install fastapi uvicorn

 % pip install fastapi uvicorn
Collecting fastapi
  Downloading fastapi-0.135.1-py3-none-any.whl.metadata (30 kB)
Collecting uvicorn
  Downloading uvicorn-0.41.0-py3-none-any.whl.metadata (6.7 kB)
....
.....
Successfully installed annotated-doc-0.0.4 annotated-types-0.7.0 anyio-4.12.1 click-8.3.1 fastapi-0.135.1 h11-0.16.0 idna-3.11 pydantic-2.12.5 pydantic-core-2.41.5 starlette-0.52.1 typing-extensions-4.15.0 typing-inspection-0.4.2 uvicorn-0.41.0

2. Import libraries and create FastAPI object

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

3. Define schema

# Input schema — what the client sends
class Book(BaseModel):
    name: str
    price: float
    in_stock: bool = True

# Update schema — all fields optional for partial updates
class BookUpdate(BaseModel):
    name: Optional[str] = None
    price: Optional[float] = None
    in_stock: Optional[bool] = None

# Output schema — what the API returns
class BookOut(BaseModel):
    id: int
    name: str
    price: float
    in_stock: bool

4. FastAPI CRUD

4.1. Create - POST
db = {}   # fake in-memory database
counter = 0

@app.post("/books", response_model=BookOut, status_code=201)
def create_item(item: Book):
    global counter
    counter += 1
    db[counter] = {"id": counter, **item.model_dump()}
    return db[counter]

Note: The in-memory database is not recommended for production.

4.2. Read - GET
# Read all
@app.get("/books", response_model=list[BookOut])
def list_items(skip: int = 0, limit: int = 10):
    return list(db.values())[skip: skip + limit]

# Read one
@app.get("/books/{item_id}", response_model=BookOut)
def get_item(item_id: int):
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Book not found")
    return db[item_id]
4.3. Update - PUT/PATCH
# PUT — full update, replaces the entire record
@app.put("/books/{item_id}", response_model=BookOut)
def update_item(item_id: int, item: Book):
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Book not found")
    db[item_id] = {"id": item_id, **item.model_dump()}
    return db[item_id]

# PATCH — partial update, only changes fields you send
@app.patch("/books/{item_id}", response_model=BookOut)
def partial_update_item(item_id: int, item: BookUpdate):
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Item not found")
    stored = db[item_id]
    updates = model_dump(exclude_unset=True)
    stored.update(updates)
    db[item_id] = stored
    return db[item_id]
4.4. Delete - DELETE
@app.delete("/books/{item_id}", status_code=204)
def delete_item(item_id: int):
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Book not found")
    del db[item_id]

5. Add steps 2 to 4 in main.py and run below command:

% uvicorn main:app --reload
INFO:     Will watch for changes in these directories: ['/Users/sathyendhra/django_project3/fastapi-project']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [29028] using StatReload
INFO:     Started server process [29030]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

9. Open “http://127.0.0.1:8000/docs” in the browser

fastAPI_Homepage

10. Create a book item using FastAPI - Swagger UI

fastAPI_Post Request

fastAPI_Post Response

11. List all books in the book store using FastAPI - Swagger UI

fastAPI_get request response

12. Update availability of a book in the book store

fastAPI_patch request

fastAPI_patch response

13. Delete a book from the book store

fastAPI_delete request response

14. List all books in the book store after update and delete operations

fastAPI_get final response

Understanding Decorators in python ( optional section for people well versed with this python feature)

As FastAPI uses decorators heavily, let’s understand this elegant Python feature. Basically, a decorator is a function that wraps another function to add behaviour before or after it runs without modifying the existing function.

Think of a decorator as a notebook cover — the notebook (your function) stays the same, the cover adds something around it.

Let us understand this with examples:

  • Without decorator
def my_decorator(func):
    def wrapper():
        print("Before")
        func()          # call the original function
        print("After")
    return wrapper

def greet():
    print("Hello!")

# manually wrap it
greet = my_decorator(greet)
greet()

# Output:
# Before
# Hello!
# After
  • With decorator
def my_decorator(func):
    def wrapper():
        print("Before")
        func()
        print("After")
    return wrapper

@my_decorator       # same as: greet = my_decorator(greet)
def greet():
    print("Hello!")

greet()

# Output:
# Before
# Hello!
# After
  • Decorator with arguments
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(times=3)
def greet():
    print("Hello!")

greet()
# Hello!
# Hello!
# Hello!

Conclusion

In this post we have explored the basics of FastAPI, equipping you to start building projects using this web framework. As next step one can learn about integrating FastAPI with SQLAlchemy, a powerful combination for building database-backed APIs and also explore writing automated tests for your API using FastAPI’s TestClient tool.

References

  1. https://fastapi.tiangolo.com/
  2. https://docs.pydantic.dev
  3. https://www.uvicorn.org
  4. https://www.starlette.io
  5. https://docs.python.org/3/glossary.html#term-decorator
  6. https://github.com/zhanymkanov/fastapi-best-practices?tab=readme-ov-file#cpu-intensive-tasks