In the previous tutorial we created a user in the database. In this one we are going to create a blog post. We will be starting with creating a Pydantic or SQLModel Schema. I am going to choose pydantic, because pydantic is more versatile. Pydantic is not strongly coupled to SQLAlchemy ORM. So, in your company if you are using some other ORM, then also there won't be any problem.
Let's create schemas/blog.py:
from pydantic import BaseModel, Field, model_validator
class CreateBlog(BaseModel):
title: str = Field(max_length=200)
slug: str = Field(max_length=200)
content: str
@model_validator(mode="before")
@classmethod
def generate_slug_from_title(cls, data: dict) -> dict:
if "title" in data:
data["slug"] = data["title"].lower().replace(" ", "-")
return data
The magic happens in the model_validator
. With mode="before"
, it runs before Pydantic validates the fields. So when someone creates a blog post with just a title and content, the validator automatically generates the slug. If you want to test it's capabilities, you can simply test it even in the terminal.
C:\fastapi\blog> docker-compose exec -it web /bin/bash
root@f035c2e69d12:/app# python
>>> from schemas.blog import CreateBlog
>>> CreateBlog(title="Hello World", content="This is some content")
CreateBlog(title='Hello World', slug='hello-world', content='This is some content')
Similarily we can add a schema to design how the API response will look like.
from typing import Optional
from pydantic import BaseModel, Field, model_validator
from .user import ShowUser
class CreateBlog(BaseModel):
title: str = Field(max_length=200)
slug: str = Field(max_length=200)
content: str
@model_validator(mode="before")
@classmethod
def generate_slug_from_title(cls, data: dict) -> dict:
if "title" in data:
data["slug"] = data["title"].lower().replace(" ", "-")
return data
class ShowBlog(BaseModel):
id: int
title: str
slug: str
content: str
is_active: bool
user_id: int
Time to focus on our crud service layer. Create a new file database/crud/blog.py:
from sqlmodel import Session
from fastapi import Depends
from database.models.blog import Blog
from apis.deps import get_db
from schemas.blog import CreateBlog
def insert_blog(blog: CreateBlog, db: Session) -> Blog:
db_blog = Blog(
title=blog.title,
slug=blog.slug,
content=blog.content,
is_active=True,
user_id=1
)
db.add(db_blog)
db.commit()
db.refresh(db_blog)
return db_blog
This function is doing the heavy lifting. We're taking our validated Pydantic model and converting it to a SQLModel database object. Notice how we're hardcoding user_id=1
- yeah, that's a temporary thing. In production, you'd get this from the authenticated user. But hey, we're keeping it simple for now!
The db.refresh(db_blog)
line is important - it updates our object with any database-generated values like the ID. Without it, you might get stale blog object.
from sqlmodel import Session
from fastapi import APIRouter, Depends, status
from apis.deps import get_db
from database.crud.blog import insert_blog
from schemas.blog import CreateBlog, ShowBlog
router = APIRouter()
@router.post("/blogs", response_model=ShowBlog, status_code=status.HTTP_201_CREATED)
def create_blog(blog: CreateBlog, db: Session = Depends(get_db)):
return insert_blog(blog, db)
This is beautiful in its simplicity! One decorator, one function, and boom - you've got a REST API endpoint. The response_model=ShowBlog
tells FastAPI exactly what to return, and it'll automatically handle the serialization for you.
That status.HTTP_201_CREATED
is the proper HTTP status code for "Hey, I created something new!" Instead of the default 200. It's these little details that make your API feel professional.
Finally, we can focus on apis/main.py file which has our pre-configured api router.
from fastapi import APIRouter
from apis.v1.user import router as user_router
from apis.v1.blog import router as blog_router
api_router = APIRouter()
api_router.include_router(user_router, prefix="/api/v1", tags=["users"])
api_router.include_router(blog_router, prefix="/api/v1", tags=["blogs"])
This is how you avoid ending up with a 500-line main.py at the root directory, that nobody wants to touch! Each feature gets its own router, and we organize them with prefixes and tags. The tags are super useful - they group your endpoints in the automatic API documentation at /docs
.
The beauty of this setup is separation of concerns. Your Pydantic models handle validation, your CRUD functions handle database operations, and your routers handle HTTP stuff. When something breaks (and it will!), you know exactly where to look.
Trying it out:
Now, every part is ready, we can start our docker compose server and visit: http://127.0.0.1:8002/docs
If you want to include the related foreignkey object simply specify it in the ShowBlog pydantic schema. e.g.
user: Optional[ShowUser] = None