Your First Project¶
This tutorial walks you through creating a simple blog API with FastAPI Smith.
Project Setup¶
Let's create a blog API with posts and comments:
Configuration¶
Use these settings for the tutorial:
- Project name:
blog-api - Database: PostgreSQL
- ORM: SQLAlchemy 2.0
- Auth: JWT
- Admin: Yes (SQLAdmin)
- Caching: Redis
- Logging: Loguru
- Package manager: uv
- Docker: Yes
Understanding the Structure¶
After generation, you'll have:
blog-api/
├── app/
│ ├── main.py # Application entry point
│ ├── config.py # Settings
│ ├── database.py # Database setup
│ ├── models/ # Database models
│ │ ├── base.py
│ │ └── user.py # User model (generated)
│ ├── schemas/ # Pydantic schemas
│ │ └── user.py
│ ├── routes/ # API routes
│ │ ├── auth.py # Auth endpoints
│ │ └── users.py # User endpoints
│ └── core/ # Core functionality
│ ├── security.py # JWT & password hashing
│ └── logging.py # Logging setup
├── migrations/ # Alembic migrations
├── tests/ # Tests
└── .env.example # Environment template
Setting Up the Environment¶
-
Copy environment file:
-
Edit
.envwith your database credentials: -
Install dependencies:
-
Create database:
-
Run migrations:
Creating the Blog Models¶
Create app/models/post.py:
from datetime import datetime
from sqlalchemy import String, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
content: Mapped[str] = mapped_column(Text)
published: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="posts")
comments: Mapped[list["Comment"]] = relationship(back_populates="post")
class Comment(Base):
__tablename__ = "comments"
id: Mapped[int] = mapped_column(primary_key=True)
content: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
post_id: Mapped[int] = mapped_column(ForeignKey("posts.id"))
post: Mapped[Post] = relationship(back_populates="comments")
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="comments")
Update app/models/user.py to add relationships:
# Add to User model
posts: Mapped[list["Post"]] = relationship(back_populates="author")
comments: Mapped[list["Comment"]] = relationship(back_populates="author")
Creating Pydantic Schemas¶
Create app/schemas/post.py:
from datetime import datetime
from pydantic import BaseModel, ConfigDict
class CommentBase(BaseModel):
content: str
class CommentCreate(CommentBase):
pass
class Comment(CommentBase):
model_config = ConfigDict(from_attributes=True)
id: int
author_id: int
created_at: datetime
class PostBase(BaseModel):
title: str
content: str
published: bool = False
class PostCreate(PostBase):
pass
class PostUpdate(BaseModel):
title: str | None = None
content: str | None = None
published: bool | None = None
class Post(PostBase):
model_config = ConfigDict(from_attributes=True)
id: int
author_id: int
created_at: datetime
comments: list[Comment] = []
Creating API Routes¶
Create app/routes/posts.py:
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..database import get_db
from ..models.post import Post, Comment
from ..models.user import User
from ..schemas.post import PostCreate, Post as PostSchema, CommentCreate
from ..core.security import get_current_user
router = APIRouter(prefix="/posts", tags=["posts"])
@router.post("/", response_model=PostSchema, status_code=status.HTTP_201_CREATED)
async def create_post(
post: PostCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
db_post = Post(**post.model_dump(), author_id=current_user.id)
db.add(db_post)
await db.commit()
await db.refresh(db_post)
return db_post
@router.get("/", response_model=list[PostSchema])
async def list_posts(
skip: int = 0,
limit: int = 10,
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Post).where(Post.published == True).offset(skip).limit(limit)
)
return result.scalars().all()
@router.get("/{post_id}", response_model=PostSchema)
async def get_post(post_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Post).where(Post.id == post_id))
post = result.scalar_one_or_none()
if not post:
raise HTTPException(status_code=404, detail="Post not found")
return post
@router.post("/{post_id}/comments", status_code=status.HTTP_201_CREATED)
async def create_comment(
post_id: int,
comment: CommentCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
db_comment = Comment(
**comment.model_dump(),
post_id=post_id,
author_id=current_user.id
)
db.add(db_comment)
await db.commit()
await db.refresh(db_comment)
return db_comment
Registering the Routes¶
Update app/main.py to include the posts router:
Running Migrations¶
Generate a migration for the new models:
Testing the API¶
-
Start the server:
-
Register a user at http://localhost:8000/docs
-
Login to get an access token
-
Create a post:
-
List posts:
Adding Tests¶
Create tests/test_posts.py:
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_post(client: AsyncClient, auth_headers: dict):
response = await client.post(
"/posts",
json={"title": "Test Post", "content": "Test content", "published": True},
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert data["title"] == "Test Post"
@pytest.mark.asyncio
async def test_list_posts(client: AsyncClient):
response = await client.get("/posts")
assert response.status_code == 200
assert isinstance(response.json(), list)
Run tests:
Next Steps¶
You now have a working blog API! Here are some ideas to extend it:
- Add post categories/tags
- Implement search functionality
- Add pagination
- Cache popular posts with Redis
- Add rate limiting
- Deploy with Docker
Check out our guides for more: