Python Fastapi Async Crud Operations Tutorial

Written by

in

Welcome to the ultimate Python FastAPI async CRUD operations tutorial! If you’re looking to build lightning‑fast, scalable REST APIs with clean, non‑blocking code, you’ve landed in the right place. In this guide we’ll walk through everything you need—from setting up a modern development environment to crafting fully asynchronous Create, Read, Update, and Delete endpoints that play nicely with a relational database. By the end, you’ll have a production‑ready FastAPI project you can extend, test, and deploy with confidence.

Why Choose FastAPI for Async CRUD?

FastAPI has quickly become the go‑to framework for Python developers who need high performance without sacrificing readability. Here’s why it shines for async CRUD workloads:

  • Native async support: Built on Starlette, FastAPI lets you define async def endpoints that run on an event loop, keeping I/O operations (like database queries) non‑blocking.
  • Automatic OpenAPI documentation: Swagger UI and ReDoc are generated instantly, making API testing a breeze.
  • Type‑hint driven validation: Pydantic models ensure request and response data are validated at runtime, reducing bugs.
  • Performance: Benchmarks show FastAPI can rival Node.js and Go for typical CRUD patterns.

Preparing Your Development Environment

Before diving into code, let’s set up a clean environment. We’ll use venv, pip, and a few essential packages.

python -m venv fastapi-env
source fastapi-env/bin/activate  # On Windows use `fastapi-env\Scripts\activate`
pip install --upgrade pip
pip install fastapi[all] uvicorn sqlalchemy asyncpg pydantic

Explanation of the key packages:

  • fastapi[all] – Installs FastAPI with optional dependencies, including uvicorn for the ASGI server.
  • uvicorn – A lightning‑fast ASGI server that runs your app.
  • sqlalchemy – The ORM that will map Python classes to database tables.
  • asyncpg – An async PostgreSQL driver; replace with aiomysql if you prefer MySQL.
  • pydantic – Handles data validation and serialization.

Designing the Database Model

For this tutorial we’ll build a simple Todo API. Using SQLAlchemy’s declarative_base we define an async‑compatible model.

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import Column, Integer, String, Boolean

DATABASE_URL = "postgresql+asyncpg://user:password@localhost/todo_db"

engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(
    bind=engine, class_=AsyncSession, expire_on_commit=False
)

Base = declarative_base()

class Todo(Base):
    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, nullable=False)
    description = Column(String, nullable=True)
    completed = Column(Boolean, default=False)

Don’t forget to create the tables before running the API:

async def init_db():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

import asyncio
asyncio.run(init_db())

Creating Pydantic Schemas

Pydantic models describe the shape of data that flows in and out of your API. They also double as OpenAPI schema definitions.

from pydantic import BaseModel

class TodoBase(BaseModel):
    title: str
    description: str | None = None
    completed: bool = False

class TodoCreate(TodoBase):
    pass

class TodoRead(TodoBase):
    id: int

    class Config:
        orm_mode = True

class TodoUpdate(BaseModel):
    title: str | None = None
    description: str | None = None
    completed: bool | None = None

Building Async CRUD Endpoints

Now we’ll wire everything together in a FastAPI app. Each endpoint uses async def and interacts with the database via an AsyncSession.

FastAPI Boilerplate

from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy.ext.asyncio import AsyncSession

app = FastAPI(title="Async Todo API", version="1.0.0")

# Dependency that provides a new DB session per request
async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        yield session

Create (POST /todos)

@app.post("/todos", response_model=TodoRead, status_code=201)
async def create_todo(todo: TodoCreate, db: AsyncSession = Depends(get_db)):
    new_todo = Todo(**todo.dict())
    db.add(new_todo)
    await db.commit()
    await db.refresh(new_todo)
    return new_todo

Read (GET /todos and GET /todos/{id})

@app.get("/todos", response_model=list[TodoRead])
async def list_todos(skip: int = 0, limit: int = 10, db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        select(Todo).offset(skip).limit(limit)
    )
    return result.scalars().all()

@app.get("/todos/{todo_id}", response_model=TodoRead)
async def get_todo(todo_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.get(Todo, todo_id)
    if not result:
        raise HTTPException(status_code=404, detail="Todo not found")
    return result

Update (PATCH /todos/{id})

@app.patch("/todos/{todo_id}", response_model=TodoRead)
async def update_todo(
    todo_id: int,
    todo_update: TodoUpdate,
    db: AsyncSession = Depends(get_db)
):
    db_todo = await db.get(Todo, todo_id)
    if not db_todo:
        raise HTTPException(status_code=404, detail="Todo not found")

    update_data = todo_update.dict(exclude_unset=True)
    for key, value in update_data.items():
        setattr(db_todo, key, value)

    await db.commit()
    await db.refresh(db_todo)
    return db_todo

Delete (DELETE /todos/{id})

@app.delete("/todos/{todo_id}", status_code=204)
async def delete_todo(todo_id: int, db: AsyncSession = Depends(get_db)):
    db_todo = await db.get(Todo, todo_id)
    if not db_todo:
        raise HTTPException(status_code=404, detail="Todo not found")
    await db.delete(db_todo)
    await db.commit()
    return None

Running and Testing the API

Start the server with Uvicorn:

uvicorn main:app --reload --host 0.0.0.0 --port 8000

FastAPI automatically generates interactive documentation at http://localhost:8000/docs (Swagger UI) and /redoc. You can also test endpoints with curl or httpie:

  • Create a todo:
    http POST http://localhost:8000/todos title="Buy groceries" completed:=false
    
  • List todos:
    curl -s http://localhost:8000/todos | jq
    
  • Update a todo:
    http PATCH http://localhost:8000/todos/1 completed:=true
    
  • Delete a todo:
    curl -X DELETE http://localhost:8000/todos/1 -i
    

Best Practices for Production‑Ready Async CRUD APIs

  • Connection pooling: Use asyncpg.create_pool or let SQLAlchemy manage pools to avoid exhausting database connections.
  • Input validation: Leverage Pydantic’s constr, conint, and custom validators to enforce business rules.
  • Error handling: Centralize exception handling with FastAPI’s exception_handler decorator for consistent JSON error responses.
  • Pagination: Implement cursor‑based pagination for large datasets instead of simple offset/limit.
  • Testing: Write async pytest fixtures that spin up a temporary SQLite or PostgreSQL instance, then use httpx.AsyncClient to hit the API.
  • Security

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *