python

What’s the Secret to Building a Slick CRUD App with FastAPI, SQLAlchemy, and Pydantic?

Mastering the Art of CRUD with FastAPI, SQLAlchemy, and Pydantic

What’s the Secret to Building a Slick CRUD App with FastAPI, SQLAlchemy, and Pydantic?

Getting into building a CRUD (Create, Read, Update, Delete) application using FastAPI, SQLAlchemy, and Pydantic can feel like a daunting task at first, but it’s super empowering once you get the hang of it. This guide is designed to walk you through it casually and in easy-to-understand terms. By the end, you’ll have a nifty app to manage your data seamlessly.

Let’s kick things off by setting up your environment. Make sure you have Python installed on your machine. You’ll also need a few packages like FastAPI, SQLAlchemy, and Pydantic. Getting these is as simple as running a pip command.

pip install fastapi sqlalchemy pydantic uvicorn

With the necessary tools in hand, let’s get into creating your database models with SQLAlchemy. Think of these models as the blueprint of your database structure.

Here’s a basic setup:

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

# Create a database engine
engine = create_engine('sqlite:///example.db')
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    items = relationship("Item", back_populates="owner")

class Item(Base):
    __tablename__ = "items"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, index=True)
    owner_id = Column(Integer, ForeignKey("users.id"))
    owner = relationship("User", back_populates="items")

Base.metadata.create_all(bind=engine)

Essentially, this code sets up a simple SQLite database with two tables: users and items. The User class represents users in the app, and each user can own multiple items, represented by the Item class.

Next, we need to define Pydantic schemas. These schemas serve as the intermediary between your database models and the API, ensuring that the data conforms to certain rules.

Here’s what that looks like:

from pydantic import BaseModel

class UserBase(BaseModel):
    email: str

class UserCreate(UserBase):
    pass

class User(UserBase):
    id: int
    items: list = []

    class Config:
        orm_mode = True

class ItemBase(BaseModel):
    title: str
    description: str

class ItemCreate(ItemBase):
    pass

class Item(ItemBase):
    id: int
    owner_id: int

    class Config:
        orm_mode = True

In this setup, UserCreate and ItemCreate schemas are used when creating new users or items. The User and Item schemas include additional fields needed when reading data from the database.

Setting up the FastAPI framework is the next step. It’s the backbone of your app, managing all the API requests.

Here’s the initial setup:

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session

app = FastAPI()

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

With this setup, we define the main app instance using FastAPI and a dependency to get a database session. This session is used to interact with the database in each request.

Now, let’s dive into the fun part: creating the CRUD operations via FastAPI path operations.

Create

Creating new users or items involves a POST request. Here’s the code for creating a user or an item:

@app.post("/users/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
    db_user = User(email=user.email)
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

@app.post("/items/", response_model=Item)
def create_item_for_user(item: ItemCreate, db: Session = Depends(get_db)):
    db_item = Item(title=item.title, description=item.description, owner_id=1)
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

Here, users and items are added to the database when we receive a POST request at /users/ or /items/. The data is committed to the database, and the newly created record is returned.

Read

Reading data uses GET requests to fetch user or item details:

@app.get("/users/", response_model=list[User])
def read_users(db: Session = Depends(get_db)):
    return db.query(User).all()

@app.get("/items/", response_model=list[Item])
def read_items(db: Session = Depends(get_db)):
    return db.query(Item).all()

@app.get("/users/{user_id}", response_model=User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.id == user_id).first()
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user

@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: int, db: Session = Depends(get_db)):
    db_item = db.query(Item).filter(Item.id == item_id).first()
    if db_item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return db_item

These endpoints allow you to fetch all users or items or a specific user or item by ID from the database.

Update

Updating existing records uses PATCH requests:

@app.patch("/users/{user_id}", response_model=User)
def update_user(user_id: int, user: UserCreate, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.id == user_id).first()
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    update_data = user.dict(exclude_unset=True)
    for key, value in update_data.items():
        setattr(db_user, key, value)
    db.commit()
    db.refresh(db_user)
    return db_user

@app.patch("/items/{item_id}", response_model=Item)
def update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):
    db_item = db.query(Item).filter(Item.id == item_id).first()
    if db_item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    update_data = item.dict(exclude_unset=True)
    for key, value in update_data.items():
        setattr(db_item, key, value)
    db.commit()
    db.refresh(db_item)
    return db_item

These endpoints allow updating user or item details. The provided data only updates the fields included in the request, leaving the rest unchanged.

Delete

Deleting records uses a DELETE request:

@app.delete("/users/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.id == user_id).first()
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    db.delete(db_user)
    db.commit()
    return {"detail": "User deleted"}

@app.delete("/items/{item_id}")
def delete_item(item_id: int, db: Session = Depends(get_db)):
    db_item = db.query(Item).filter(Item.id == item_id).first()
    if db_item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    db.delete(db_item)
    db.commit()
    return {"detail": "Item deleted"}

These endpoints enable deleting users or items from the database. If the user or item does not exist, a 404 error is raised.

Handling relationships between models ensures that the related data is correctly loaded. For example, when fetching a user, you might also want to fetch their related items. Here’s how you can handle relationships:

from sqlalchemy.orm import selectinload

@app.get("/users/{user_id}", response_model=User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = db.query(User).options(selectinload(User.items)).filter(User.id == user_id).first()
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user

Using selectinload, you can load related items in the same database query, avoiding multiple queries and improving performance.

If you’re keen on boosting performance with asynchronous operations, async SQLAlchemy is your friend. Here’s a brief peek into using it:

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession

async_engine = create_async_engine('sqlite+aiosqlite:///example.db')

async def get_db():
    async with AsyncSession(async_engine) as session:
        yield session

@app.get("/users/")
async def read_users(db: AsyncSession = Depends(get_db)):
    return await db.execute(select(User))

This setup uses create_async_engine for async operations, making your app faster and more efficient.

In conclusion, building a CRUD application with FastAPI, SQLAlchemy, and Pydantic is quite straightforward. You’ll define your database models with SQLAlchemy, create Pydantic schemas for validation, and whip up FastAPI path operations to manage the data. Keep an eye out for relationships and opt for async operations when needed. This ensures your app runs smooth and efficient.

Now you’re all set to create your own CRUD application. Happy coding!

Keywords: FastAPI, SQLAlchemy, Pydantic, CRUD application, Python, FastAPI tutorial, SQLAlchemy tutorial, Pydantic schemas, API development, Python web framework



Similar Posts
Blog Image
5 Essential Python Libraries for Efficient Data Preprocessing

Discover 5 essential Python libraries for efficient data preprocessing. Learn how Pandas, Scikit-learn, NumPy, Dask, and category_encoders can streamline your workflow. Boost your data science skills today!

Blog Image
Building a Domain-Specific Language in Python Using PLY and Lark

Domain-specific languages (DSLs) simplify complex tasks in specific domains. Python tools like PLY and Lark enable custom DSL creation, enhancing code expressiveness and readability. DSLs bridge the gap between developers and domain experts, making collaboration easier.

Blog Image
Ready to Build APIs Faster than The Flash?

Harness Speed and Scalability with FastAPI and PostgreSQL: The API Dream Team

Blog Image
Why Haven't You Tried This Perfect Duo for Building Flawless APIs Yet?

Building Bulletproof APIs: FastAPI and Pydantic as Your Dynamic Duo

Blog Image
How to Tame Any API Response with Marshmallow: Advanced Deserialization Techniques

Marshmallow simplifies API response handling in Python, offering easy deserialization, nested schemas, custom validation, and advanced features like method fields and pre-processing hooks. It's a powerful tool for taming complex data structures.

Blog Image
Is Your FastAPI Database Slowing You Down? Dive Into These Performance Hacks

Turbocharging Your FastAPI App: Mastering Database Tricks for Speed and Reliability