Blog Pattern¶
A complete blog backend with posts, comments, moderation, and publish workflow.
Quick Start¶
aksara startproject myblog --template blog
cd myblog
pip install -e ".[dev]"
aksara makemigrations --app app.models
aksara migrate
aksara dev
Models¶
Post¶
class Post(Model):
"""Blog post with publish workflow."""
title = fields.String(max_length=200, ai_description="Post title")
slug = fields.String(
max_length=200,
unique=True,
ai_description="URL-friendly slug",
)
content = fields.Text(nullable=True, ai_description="Post body (Markdown)")
tags = fields.JSON(nullable=True, ai_description="List of tags")
is_published = fields.Boolean(default=False, ai_description="Publication status")
view_count = fields.Integer(default=0, ai_agent_writable=False)
author_email = fields.Email(nullable=True)
created_at = fields.DateTime(auto_now_add=True)
updated_at = fields.DateTime(auto_now=True)
class Meta:
table_name = "posts"
ai_name = "Post"
ai_agent_exposed = True
Comment¶
class Comment(Model):
"""Comment with moderation."""
post = fields.ForeignKey(Post, on_delete="CASCADE", related_name="comments")
author_name = fields.String(max_length=100)
author_email = fields.Email()
body = fields.Text(ai_description="Comment body")
is_approved = fields.Boolean(default=False, ai_description="Moderation status")
created_at = fields.DateTime(auto_now_add=True)
class Meta:
table_name = "comments"
ai_name = "Comment"
API Endpoints¶
Posts¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/posts/ |
List posts (with filtering) |
| POST | /api/posts/ |
Create a post |
| GET | /api/posts/{id}/ |
Get post details |
| PUT | /api/posts/{id}/ |
Update a post |
| DELETE | /api/posts/{id}/ |
Delete a post |
| POST | /api/posts/{id}/publish/ |
Publish a post |
| POST | /api/posts/{id}/unpublish/ |
Unpublish a post |
| POST | /api/posts/{id}/view/ |
Increment view count |
Comments¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/comments/ |
List comments |
| POST | /api/comments/ |
Create a comment |
| GET | /api/comments/{id}/ |
Get comment details |
| DELETE | /api/comments/{id}/ |
Delete a comment |
| POST | /api/comments/{id}/approve/ |
Approve a comment |
| POST | /api/comments/{id}/reject/ |
Reject a comment |
Authentication (v0.5.8)¶
The blog example includes API key authentication for protected endpoints.
Setup¶
# settings.py
import os
BLOG_API_KEY = os.getenv("BLOG_API_KEY", "dev-blog-key")
DEFAULT_PAGE_SIZE = 10
MAX_PAGE_SIZE = 100
Auth Module¶
# auth.py
from fastapi import HTTPException, Header
from . import settings
def verify_api_key(api_key: str) -> bool:
"""Verify API key against configured key."""
return api_key == settings.BLOG_API_KEY
async def require_api_key(x_api_key: str = Header(..., alias="X-API-Key")) -> str:
"""FastAPI dependency for API key authentication."""
if not verify_api_key(x_api_key):
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
Usage¶
from fastapi import Depends
from .auth import require_api_key
class PostViewSet(ModelViewSet):
dependencies = [Depends(require_api_key)]
# All endpoints now require X-API-Key header
Example Request¶
Pagination & Ordering (v0.5.8)¶
List endpoints support pagination and ordering via query parameters.
Query Parameters¶
| Parameter | Default | Description |
|---|---|---|
page |
1 | Page number (1-based) |
page_size |
10 | Items per page (max 100) |
order_by |
created_at |
Field to sort by |
is_published |
- | Filter by publish status |
Ordering¶
- Ascending:
?order_by=title - Descending:
?order_by=-created_at
Example Requests¶
# First page, 10 posts, newest first (default)
curl http://localhost:8000/api/posts/ \
-H "X-API-Key: dev-blog-key"
# Second page, 20 posts per page
curl "http://localhost:8000/api/posts/?page=2&page_size=20" \
-H "X-API-Key: dev-blog-key"
# Sort by title ascending
curl "http://localhost:8000/api/posts/?order_by=title" \
-H "X-API-Key: dev-blog-key"
# Published posts only, oldest first
curl "http://localhost:8000/api/posts/?is_published=true&order_by=created_at" \
-H "X-API-Key: dev-blog-key"
Response Format¶
AI-Ready Endpoints (v0.5.8)¶
The blog example includes an AI-aware endpoint for automatic tag suggestions.
AI Suggest Tags¶
@action(
detail=True,
methods=["GET"],
path="ai-suggest-tags",
name="suggest_tags_for_post",
description="Suggest tags for a blog post based on its title and content.",
ai_exposed=True,
)
async def ai_suggest_tags(self, pk: str, request: Request):
"""AI Tool: Suggest tags for a blog post."""
post = await self.model.objects.get(id=pk)
# Extract keywords from title and content
text = f"{post.title} {post.content or ''}"
words = text.lower().split()
# Simple keyword extraction (filter stopwords)
keywords = [w for w in words if len(w) > 4 and w not in STOPWORDS]
candidate_tags = list(set(keywords))[:10]
return {
"post_id": str(post.id),
"post_title": post.title,
"existing_tags": post.tags or [],
"candidate_tags": candidate_tags,
}
Example Request¶
Response¶
{
"post_id": "abc123",
"post_title": "Getting Started with Python",
"existing_tags": ["python"],
"candidate_tags": ["programming", "tutorial", "beginner", "started", "getting"]
}
AI Tools Discovery¶
This endpoint is discoverable at /ai/tools with ai_exposed=True:
{
"name": "suggest_tags_for_post",
"description": "Suggest tags for a blog post based on its title and content.",
"endpoint": "/api/posts/{id}/ai-suggest-tags/"
}
Custom Actions¶
Publish Workflow¶
class PostViewSet(ModelViewSet):
@action(detail=True, methods=["POST"])
async def publish(self, pk: str, request: Request):
"""Publish a post (make it visible)."""
post = await self.model.objects.get(id=pk)
if not post.content:
return {"error": "Cannot publish post without content"}
post.is_published = True
await post.save()
return {"status": "published", "id": str(post.id)}
@action(detail=True, methods=["POST"])
async def unpublish(self, pk: str, request: Request):
"""Unpublish a post (hide from public)."""
post = await self.model.objects.get(id=pk)
post.is_published = False
await post.save()
return {"status": "unpublished", "id": str(post.id)}
Comment Moderation¶
class CommentViewSet(ModelViewSet):
@action(detail=True, methods=["POST"])
async def approve(self, pk: str, request: Request):
"""Approve a comment for display."""
comment = await self.model.objects.get(id=pk)
comment.is_approved = True
await comment.save()
return {"status": "approved", "id": str(comment.id)}
@action(detail=True, methods=["POST"])
async def reject(self, pk: str, request: Request):
"""Reject/hide a comment."""
comment = await self.model.objects.get(id=pk)
comment.is_approved = False
await comment.save()
return {"status": "rejected", "id": str(comment.id)}
Example Requests¶
Create a Post¶
curl -X POST http://localhost:8000/api/posts/ \
-H "Content-Type: application/json" \
-d '{
"title": "Hello World",
"slug": "hello-world",
"content": "# Welcome\n\nThis is my first post.",
"tags": ["welcome", "intro"]
}'
Publish the Post¶
Add a Comment¶
curl -X POST http://localhost:8000/api/comments/ \
-H "Content-Type: application/json" \
-d '{
"post_id": 1,
"author_name": "Jane",
"author_email": "jane@example.com",
"body": "Great post!"
}'
Approve the Comment¶
Serializers¶
PostSerializer (Full)¶
class PostSerializer(ModelSerializer):
class Meta:
model = Post
fields = [
"id", "title", "slug", "content", "tags",
"is_published", "view_count", "author_email",
"created_at", "updated_at",
]
read_only_fields = ["id", "view_count", "created_at", "updated_at"]
PostListSerializer (Compact)¶
class PostListSerializer(ModelSerializer):
"""Compact serializer for list views."""
class Meta:
model = Post
fields = ["id", "title", "slug", "is_published", "created_at"]
Admin Configuration¶
class PostAdmin(ModelAdmin):
list_display = ["title", "slug", "is_published", "view_count", "created_at"]
list_filter = ["is_published"]
search_fields = ["title", "content"]
class CommentAdmin(ModelAdmin):
list_display = ["post", "author_name", "is_approved", "created_at"]
list_filter = ["is_approved"]
search_fields = ["author_name", "body"]
Extending the Pattern¶
Add Categories¶
class Category(Model):
name = fields.String(max_length=100)
slug = fields.String(max_length=100, unique=True)
class Meta:
table_name = "categories"
class Post(Model):
# ... existing fields ...
category = fields.ForeignKey(Category, nullable=True, on_delete="SET_NULL")
Add Featured Images¶
class Post(Model):
# ... existing fields ...
featured_image_url = fields.String(max_length=500, nullable=True)
featured_image_alt = fields.String(max_length=200, nullable=True)
Add SEO Fields¶
class Post(Model):
# ... existing fields ...
meta_title = fields.String(max_length=60, nullable=True)
meta_description = fields.String(max_length=160, nullable=True)
Next Steps¶
- CRM Pattern - Sales pipeline
- Multitenant Pattern - SaaS backend
- ViewSet Actions - Custom actions