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 APIs with Python, FastAPI is the framework that delivers both performance and developer happiness. In this guide, we’ll walk through everything you need to know to implement asynchronous Create, Read, Update, and Delete (CRUD) endpoints using FastAPI, SQLAlchemy, and an async‑compatible database driver. By the end, you’ll have a production‑ready codebase that you can extend for any web or mobile application.

Why Choose FastAPI for Async CRUD?

FastAPI stands out for several reasons that make it perfect for async CRUD operations:

  • Native async support: Built on Starlette, FastAPI lets you define async def endpoints that run concurrently without blocking the event loop.
  • Automatic data validation: Pydantic models validate request bodies and responses, reducing boilerplate code.
  • OpenAPI documentation: Swagger UI and ReDoc are generated automatically, giving you interactive API docs out of the box.
  • High performance: Benchmarks show FastAPI can handle thousands of requests per second, rivaling Node.js and Go.

Project Setup: Installing the Essentials

Before we dive into code, set up a virtual environment and install the required packages:

python -m venv venv
source venv/bin/activate   # On Windows use `venv\Scripts\activate`
pip install fastapi[all]   # Includes Uvicorn, Pydantic, etc.
pip install sqlalchemy[asyncio] aiosqlite   # Async SQLite driver (or asyncpg for PostgreSQL)

We’ll use SQLite for simplicity, but the same patterns apply to PostgreSQL, MySQL, or any async‑compatible database.

Defining the Database Model with SQLAlchemy

FastAPI works seamlessly with SQLAlchemy’s async API. Below is a minimal Item model that represents a typical resource in a CRUD API.

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Item(Base):
    __tablename__ = "items"

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

Creating an Async Engine and Session

Use create_async_engine and async_sessionmaker to manage connections without blocking the event loop.

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

DATABASE_URL = "sqlite+aiosqlite:///./test.db"

engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)

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

Designing Pydantic Schemas for Validation

Pydantic models define the shape of request and response payloads. Separate schemas for creation, update, and reading keep the API clean.

from pydantic import BaseModel

class ItemBase(BaseModel):
    name: str
    description: str | None = None

class ItemCreate(ItemBase):
    pass

class ItemUpdate(BaseModel):
    name: str | None = None
    description: str | None = None

class ItemRead(ItemBase):
    id: int

    class Config:
        orm_mode = True

Building Async CRUD Endpoints

Now we’ll wire everything together in a FastAPI router. Each endpoint uses async def and the async session to interact with the database.

1. Create (POST /items)

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession

router = APIRouter()

async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        yield session

@router.post("/items", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemCreate, db: AsyncSession = Depends(get_db)):
    db_item = Item(**item.dict())
    db.add(db_item)
    await db.commit()
    await db.refresh(db_item)
    return db_item

2. Read All (GET /items)

@router.get("/items", response_model=list[ItemRead])
async def read_items(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Item).offset(skip).limit(limit))
    items = result.scalars().all()
    return items

3. Read One (GET /items/{item_id})

@router.get("/items/{item_id}", response_model=ItemRead)
async def read_item(item_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Item).where(Item.id == item_id))
    item = result.scalar_one_or_none()
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

4. Update (PUT /items/{item_id})

@router.put("/items/{item_id}", response_model=ItemRead)
async def update_item(item_id: int, item_update: ItemUpdate, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Item).where(Item.id == item_id))
    db_item = result.scalar_one_or_none()
    if not db_item:
        raise HTTPException(status_code=404, detail="Item not found")

    for field, value in item_update.dict(exclude_unset=True).items():
        setattr(db_item, field, value)

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

5. Delete (DELETE /items/{item_id})

@router.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Item).where(Item.id == item_id))
    db_item = result.scalar_one_or_none()
    if not db_item:
        raise HTTPException(status_code=404, detail="Item not found")
    await db.delete(db_item)
    await db.commit()
    return None

Integrating the Router into the FastAPI Application

With the CRUD router ready, create the main app file, include the router, and start the server.

from fastapi import FastAPI
import uvicorn

app = FastAPI(title="Async CRUD with FastAPI", version="1.0.0")
app.include_router(router, prefix="/api")

@app.on_event("startup")
async def on_startup():
    await init_db()

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

Testing the Async Endpoints

FastAPI’s built‑in TestClient works with async routes when you use AsyncClient from httpx. Here’s a quick example:

import pytest
from httpx import AsyncClient
from main import app

@pytest.mark.asyncio
async def test_create_item():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.post("/api/items", json={"name": "Sample", "description": "Demo"})
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Sample"
    assert "id" in data

Best Practices for Production‑Ready Async CRUD APIs

  • Use connection pooling: Async drivers like asyncpg support pools that keep connections alive, reducing latency.
  • Validate input thoroughly: Leverage Pydantic’s validators and custom error handlers for clear client feedback.
  • Implement pagination: Return skip and limit parameters (as shown) to avoid massive payloads.
  • Secure endpoints: Add OAuth2 or JWT authentication via FastAPI’s Depends system.
  • Log and monitor: Integrate structured logging (e.g., loguru) and health‑check endpoints for observability.

Deploying FastAPI Async Apps

When you’re ready to go live, consider these deployment options:

  1. Uvicorn + Gunicorn: Use gunicorn -k uvicorn.workers.UvicornWorker app:app for multi‑worker scaling.
  2. Docker: Containerize the app with a lightweight python:3.12-slim base image and expose port 8000.
  3. Serverless: Platforms like AWS Lambda (via Mangum) or Azure Functions support ASGI, letting you run FastAPI without managing servers.

Conclusion

Building async CRUD operations with Python FastAPI is both straightforward and powerful. By combining FastAPI’s declarative routing, Pydantic validation, and SQLAlchemy’s async ORM, you get a clean, high‑performance API that scales effortlessly. Follow the steps in this tutorial, adopt the best practices, and you’ll be ready to ship production‑grade services that developers love to use and maintain.

Comments

Leave a Reply

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