This is going to be the final part of CRUD.
Alright, time to complete our CRUD quadlogy! with the most destructive operation - DELETE! This is where we permanently remove blog posts from existence. No undo button, no recycle bin, just skadoosh - gone forever! 💥
The CRUD Layer Finale
Let's add our delete function to database/crud/blog.py:
def delete_blog_by_slug(slug: str, db: Session) -> Optional[bool]:
statement = select(Blog).where(Blog.slug == slug)
result = db.exec(statement)
db_blog = result.first()
if not db_blog:
return None
db.delete(db_blog)
db.commit()
return True
The Familiar Pattern: We start with the same old pattern- find the blog by slug using select()
and where()
. By now, this should feel like muscle memory!
The Point of No Return: db.delete(db_blog)
marks the record for deletion. But here's the thing - it's not actually gone yet! SQLAlchemy or SQLModel just marks it for deletion in the current transaction.
The Final Commit: db.commit()
is where the magic (or tragedy) happens. This is when the blog actually gets kicked out from the database.
The Route Layer
Now for the DELETE endpoint in apis/v1/blog.py:
@router.delete("/blogs/{slug}", status_code=status.HTTP_200_OK)
def delete_blog(slug: str, db: Session = Depends(get_db)):
if delete_blog_by_slug(slug, db):
return {"detail": "Blog deleted successfully"}
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blog not found")
Status Code Choice: We're using 200 OK
instead of 204 No Content
. Both are valid for DELETE operations, but 204 is often preferred since there's literally no content to return. But returning a success message is nice for frontend developers, so I went with 200.
No Response Model: Unlike our other endpoints, this DELETE route doesn't have a response_model
. That's actually fine since we're just returning a simple dict, but it's inconsistent with our other endpoints.
Alternative Approaches
Return the Deleted Object: Some APIs return the deleted object before destroying it. Useful for "undo" functionality:
def delete_blog_by_slug(slug: str, db: Session) -> Blog:
statement = select(Blog).where(Blog.slug == slug)
result = db.exec(statement)
db_blog = result.first()
if not db_blog:
return None
# Store the blog data before deleting
deleted_blog = db_blog
db.delete(db_blog)
db.commit()
return deleted_blog
Soft Delete: In many real-world apps, you don't actually delete data - you just mark it as "deleted" with a flag or timestamp. Users can't see it, but you can restore it if needed:
def soft_delete_blog_by_slug(slug: str, db: Session = Depends(get_db)) -> Blog:
# ... find blog logic ...
db_blog.is_deleted = True
db_blog.deleted_at = datetime.utcnow()
db.commit()
return db_blog
Testing it out:
Time to test our delete functionality:
What We've Built
Congratulations! We now have a complete CRUD API:
- ✅ Create blogs with POST
- ✅ Read blogs with GET (single and list)
- ✅ Update blogs with PUT
- ✅ Delete blogs with DELETE