Step 2: Create CRUD Views¶
In this step, you'll learn how to create a complete REST API with CRUD operations using APIViewSet.
What You'll Learn¶
- How to create a basic ViewSet
- Understanding auto-generated endpoints
- Customizing query parameters
- Adding custom endpoints
- Working with request context
- Handling errors
Prerequisites¶
Make sure you've completed:
You should have the Article, Author, Category, and Tag models defined.
Basic ViewSet¶
Let's create a simple API for the Article model:
# views.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
from .models import Article
# Create API instance
api = NinjaAIO(
title="Blog API",
version="1.0.0",
description="A simple blog API built with Django Ninja Aio CRUD"
)
class ArticleViewSet(APIViewSet):
model = Article
api = api
# Register the ViewSet
ArticleViewSet().add_views_to_route()
That's it! You now have a complete CRUD API with 5 endpoints.
Configure URLs¶
Add the API to your Django URLs:
# urls.py
from django.contrib import admin
from django.urls import path
from myapp.views import api
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls),
]
Auto-Generated Endpoints¶
The ViewSet automatically generates these endpoints:
| Method | Endpoint | Description | Request Body | Response |
|---|---|---|---|---|
GET |
/api/article/ |
List all articles (paginated) | None | {count, next, previous, results} |
POST |
/api/article/ |
Create new article | Article data | Created article |
GET |
/api/article/{id} |
Retrieve single article | None | Article data |
PATCH |
/api/article/{id}/ |
Update article | Partial article data | Updated article |
DELETE |
/api/article/{id}/ |
Delete article | None | None (204) |
Test Your API¶
Start the development server:
Visit http://localhost:8000/api/docs to see the auto-generated Swagger UI documentation.
Creating Multiple ViewSets¶
Let's add APIs for all our models:
# views.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
from .models import Article, Author, Category, Tag
api = NinjaAIO(title="Blog API", version="1.0.0")
class AuthorViewSet(APIViewSet):
model = Author
api = api
class CategoryViewSet(APIViewSet):
model = Category
api = api
class TagViewSet(APIViewSet):
model = Tag
api = api
class ArticleViewSet(APIViewSet):
model = Article
api = api
# Register all ViewSets
AuthorViewSet().add_views_to_route()
CategoryViewSet().add_views_to_route()
TagViewSet().add_views_to_route()
ArticleViewSet().add_views_to_route()
Now you have complete CRUD APIs for all models:
/api/author//api/category//api/tag//api/article/
Adding Query Parameters¶
Let's add filtering to the Article list endpoint:
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"is_published": (bool, None),
"author": (int, None),
"category": (int, None),
"search": (str, None),
}
async def query_params_handler(self, queryset, filters):
# Filter by published status
if filters.get("is_published") is not None:
queryset = queryset.filter(is_published=filters["is_published"])
# Filter by author
if filters.get("author"):
queryset = queryset.filter(author_id=filters["author"])
# Filter by category
if filters.get("category"):
queryset = queryset.filter(category_id=filters["category"])
# Search in title and content
if filters.get("search"):
from django.db.models import Q
search_term = filters["search"]
queryset = queryset.filter(
Q(title__icontains=search_term) |
Q(content__icontains=search_term)
)
return queryset
ArticleViewSet().add_views_to_route()
Using Query Parameters¶
Now you can filter articles:
# Get published articles
GET /api/article/?is_published=true
# Get articles by specific author
GET /api/article/?author=5
# Get articles in specific category
GET /api/article/?category=3
# Search articles
GET /api/article/?search=django
# Combine filters
GET /api/article/?is_published=true&author=5&category=3
# With pagination
GET /api/article/?is_published=true&page=2&page_size=20
Custom Endpoints¶
Add custom endpoints beyond CRUD:
class ArticleViewSet(APIViewSet):
model = Article
api = api
def views(self):
"""Define custom endpoints"""
# Publish an article
@self.router.post("/{pk}/publish/")
async def publish(request, pk: int):
article = await Article.objects.aget(pk=pk)
article.is_published = True
from django.utils import timezone
article.published_at = timezone.now()
await article.asave()
return {
"message": "Article published successfully",
"published_at": article.published_at
}
# Unpublish an article
@self.router.post("/{pk}/unpublish/")
async def unpublish(request, pk: int):
article = await Article.objects.aget(pk=pk)
article.is_published = False
article.published_at = None
await article.asave()
return {"message": "Article unpublished successfully"}
# Increment view count
@self.router.post("/{pk}/view/")
async def increment_views(request, pk: int):
article = await Article.objects.aget(pk=pk)
article.views += 1
await article.asave(update_fields=["views"])
return {"views": article.views}
# Get article statistics
@self.router.get("/stats/")
async def stats(request):
from django.db.models import Count, Avg, Sum
total = await Article.objects.acount()
published = await Article.objects.filter(is_published=True).acount()
# Use sync_to_async for aggregate
from asgiref.sync import sync_to_async
avg_views = await sync_to_async(
lambda: Article.objects.aggregate(avg=Avg("views"))
)()
total_views = await sync_to_async(
lambda: Article.objects.aggregate(total=Sum("views"))
)()
return {
"total_articles": total,
"published_articles": published,
"draft_articles": total - published,
"average_views": avg_views["avg"] or 0,
"total_views": total_views["total"] or 0,
}
# Get popular articles
@self.router.get("/popular/")
async def popular(request, limit: int = 10):
articles = []
async for article in Article.objects.filter(
is_published=True
).order_by("-views")[:limit]:
articles.append(article)
# Serialize articles
from ninja_aio.models import ModelUtil
util = ModelUtil(Article)
schema = Article.generate_read_s()
results = []
for article in articles:
data = await util.read_s(request, article, schema)
results.append(data)
return results
ArticleViewSet().add_views_to_route()
Custom Endpoint URLs¶
Your custom endpoints are now available:
# Publish article
POST /api/article/1/publish/
# Unpublish article
POST /api/article/1/unpublish/
# Increment views
POST /api/article/1/view/
# Get statistics
GET /api/article/stats/
# Get popular articles (top 10)
GET /api/article/popular/
# Get top 20
GET /api/article/popular/?limit=20
Request Context¶
Access request information in your ViewSet:
class ArticleViewSet(APIViewSet):
model = Article
api = api
def views(self):
@self.router.get("/my-articles/")
async def my_articles(request):
"""Get articles by current user"""
# Access authenticated user
user = request.auth
# Get user's articles
articles = []
async for article in Article.objects.filter(author=user):
articles.append(article)
# Serialize
from ninja_aio.models import ModelUtil
util = ModelUtil(Article)
schema = Article.generate_read_s()
results = []
for article in articles:
data = await util.read_s(request, article, schema)
results.append(data)
return results
@self.router.post("/")
async def create_article(request, data: Article.generate_create_s()):
"""Override create to set author from request"""
# Set author from authenticated user
data.author = request.auth.id
# Use default create logic
from ninja_aio.models import ModelUtil
util = ModelUtil(Article)
schema = Article.generate_read_s()
return await util.create_s(request, data, schema)
ArticleViewSet().add_views_to_route()
Filtering by User¶
Automatically filter queryset based on user:
class Article(ModelSerializer):
# ... fields ...
@classmethod
async def queryset_request(cls, request):
"""Filter articles based on user"""
qs = cls.objects.select_related('author', 'category').prefetch_related('tags')
# If user is not authenticated, show only published
if not request.auth:
return qs.filter(is_published=True)
# If user is admin, show all
user = request.auth
if user.is_staff:
return qs
# Regular users see published + their own drafts
from django.db.models import Q
return qs.filter(
Q(is_published=True) | Q(author=user)
)
class ArticleViewSet(APIViewSet):
model = Article
api = api
# queryset_request is automatically called for all operations
ArticleViewSet().add_views_to_route()
Custom Pagination¶
Override default pagination:
from ninja.pagination import PageNumberPagination
class LargePagePagination(PageNumberPagination):
page_size = 50 # Default 50 items per page
max_page_size = 200 # Allow up to 200 items
class ArticleViewSet(APIViewSet):
model = Article
api = api
pagination_class = LargePagePagination
ArticleViewSet().add_views_to_route()
Now list endpoint uses custom pagination:
# Default 50 items
GET /api/article/
# Custom page size
GET /api/article/?page_size=100
# Page 2
GET /api/article/?page=2&page_size=50
Ordering¶
Add ordering to list endpoint:
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"is_published": (bool, None),
"ordering": (str, "-created_at"), # Default: newest first
}
async def query_params_handler(self, queryset, filters):
# Apply published filter
if filters.get("is_published") is not None:
queryset = queryset.filter(is_published=filters["is_published"])
# Apply ordering
ordering = filters.get("ordering", "-created_at")
# Validate ordering field
valid_fields = [
"created_at", "-created_at",
"title", "-title",
"views", "-views",
"published_at", "-published_at"
]
if ordering in valid_fields:
queryset = queryset.order_by(ordering)
return queryset
ArticleViewSet().add_views_to_route()
Usage:
# Newest first (default)
GET /api/article/
# Oldest first
GET /api/article/?ordering=created_at
# By title A-Z
GET /api/article/?ordering=title
# By title Z-A
GET /api/article/?ordering=-title
# Most viewed
GET /api/article/?ordering=-views
# Recently published
GET /api/article/?ordering=-published_at
Error Handling¶
Handle errors gracefully:
from ninja_aio.exceptions import SerializeError, NotFoundError
class ArticleViewSet(APIViewSet):
model = Article
api = api
def views(self):
@self.router.post("/{pk}/publish/")
async def publish(request, pk: int):
try:
article = await Article.objects.aget(pk=pk)
except Article.DoesNotExist:
raise NotFoundError(self.model)
# Check if already published
if article.is_published:
raise SerializeError(
{"article": "already published"},
status_code=400
)
# Publish
article.is_published = True
from django.utils import timezone
article.published_at = timezone.now()
await article.asave()
return {
"message": "Article published successfully",
"published_at": article.published_at
}
ArticleViewSet().add_views_to_route()
Disabling Endpoints¶
Disable specific CRUD operations:
class CategoryViewSet(APIViewSet):
model = Category
api = api
# Disable delete (categories can't be deleted)
disable_delete = True
# Disable update (categories are immutable)
disable_update = True
CategoryViewSet().add_views_to_route()
Now only these endpoints are available:
GET /api/category/- ListPOST /api/category/- CreateGET /api/category/{id}- Retrieve
Response Customization¶
Customize response format:
class ArticleViewSet(APIViewSet):
model = Article
api = api
def views(self):
@self.router.get("/{pk}/")
async def retrieve(request, pk: int):
"""Custom retrieve with additional data"""
article = await Article.objects.select_related(
'author', 'category'
).prefetch_related('tags').aget(pk=pk)
# Serialize article
from ninja_aio.models import ModelUtil
util = ModelUtil(Article)
schema = Article.generate_read_s()
article_data = await util.read_s(request, article, schema)
# Get related articles
related = []
async for rel_article in Article.objects.filter(
category=article.category,
is_published=True
).exclude(pk=pk)[:5]:
rel_data = await util.read_s(request, rel_article, schema)
related.append(rel_data)
# Get author's other articles
author_articles = []
async for auth_article in Article.objects.filter(
author=article.author,
is_published=True
).exclude(pk=pk)[:5]:
auth_data = await util.read_s(request, auth_article, schema)
author_articles.append(auth_data)
return {
"article": article_data,
"related_articles": related,
"author_articles": author_articles,
"meta": {
"total_views": article.views,
"author_article_count": await Article.objects.filter(
author=article.author
).acount()
}
}
ArticleViewSet().add_views_to_route()
Complete Example¶
Here's a complete ViewSet with all features:
# views.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
from ninja.pagination import PageNumberPagination
from ninja_aio.exceptions import SerializeError, NotFoundError
from .models import Article, Author, Category, Tag
from django.db.models import Q
api = NinjaAIO(
title="Blog API",
version="1.0.0",
description="A complete blog API"
)
class CustomPagination(PageNumberPagination):
page_size = 20
max_page_size = 100
class AuthorViewSet(APIViewSet):
model = Author
api = api
class CategoryViewSet(APIViewSet):
model = Category
api = api
class TagViewSet(APIViewSet):
model = Tag
api = api
class ArticleViewSet(APIViewSet):
model = Article
api = api
pagination_class = CustomPagination
query_params = {
"is_published": (bool, None),
"author": (int, None),
"category": (int, None),
"tag": (int, None),
"search": (str, None),
"ordering": (str, "-created_at"),
}
async def query_params_handler(self, queryset, filters):
# Published filter
if filters.get("is_published") is not None:
queryset = queryset.filter(is_published=filters["is_published"])
# Author filter
if filters.get("author"):
queryset = queryset.filter(author_id=filters["author"])
# Category filter
if filters.get("category"):
queryset = queryset.filter(category_id=filters["category"])
# Tag filter
if filters.get("tag"):
queryset = queryset.filter(tags__id=filters["tag"])
# Search
if filters.get("search"):
search = filters["search"]
queryset = queryset.filter(
Q(title__icontains=search) |
Q(content__icontains=search) |
Q(excerpt__icontains=search)
)
# Ordering
ordering = filters.get("ordering", "-created_at")
valid_orderings = [
"created_at", "-created_at",
"title", "-title",
"views", "-views",
"published_at", "-published_at"
]
if ordering in valid_orderings:
queryset = queryset.order_by(ordering)
return queryset
def views(self):
# Publish article
@self.router.post("/{pk}/publish/")
async def publish(request, pk: int):
try:
article = await Article.objects.aget(pk=pk)
except Article.DoesNotExist:
raise NotFoundError(self.model)
if article.is_published:
raise SerializeError(
{"article": "already published"},
status_code=400
)
article.is_published = True
from django.utils import timezone
article.published_at = timezone.now()
await article.asave()
return {"message": "Article published", "published_at": article.published_at}
# Unpublish article
@self.router.post("/{pk}/unpublish/")
async def unpublish(request, pk: int):
try:
article = await Article.objects.aget(pk=pk)
except Article.DoesNotExist:
raise NotFoundError(self.model)
article.is_published = False
article.published_at = None
await article.asave()
return {"message": "Article unpublished"}
# Increment views
@self.router.post("/{pk}/view/")
async def view(request, pk: int):
try:
article = await Article.objects.aget(pk=pk)
except Article.DoesNotExist:
raise NotFoundError(self.model)
article.views += 1
await article.asave(update_fields=["views"])
return {"views": article.views}
# Statistics
@self.router.get("/stats/")
async def stats(request):
from django.db.models import Count, Avg, Sum
from asgiref.sync import sync_to_async
total = await Article.objects.acount()
published = await Article.objects.filter(is_published=True).acount()
avg_views = await sync_to_async(
lambda: Article.objects.aggregate(avg=Avg("views"))
)()
total_views = await sync_to_async(
lambda: Article.objects.aggregate(total=Sum("views"))
)()
return {
"total_articles": total,
"published": published,
"drafts": total - published,
"avg_views": avg_views["avg"] or 0,
"total_views": total_views["total"] or 0,
}
# Popular articles
@self.router.get("/popular/")
async def popular(request, limit: int = 10):
articles = []
async for article in Article.objects.filter(
is_published=True
).order_by("-views")[:limit]:
articles.append(article)
from ninja_aio.models import ModelUtil
util = ModelUtil(Article)
schema = Article.generate_read_s()
results = []
for article in articles:
data = await util.read_s(request, article, schema)
results.append(data)
return results
# Register ViewSets
AuthorViewSet().add_views_to_route()
CategoryViewSet().add_views_to_route()
TagViewSet().add_views_to_route()
ArticleViewSet().add_views_to_route()
Testing Your API¶
Test your endpoints using curl, httpie, or the Swagger UI:
# List articles
curl http://localhost:8000/api/article/
# Create article
curl -X POST http://localhost:8000/api/article/ \
-H "Content-Type: application/json" \
-d '{
"title": "My Article",
"content": "Content here...",
"author": 1,
"category": 2
}'
# Get article
curl http://localhost:8000/api/article/1
# Update article
curl -X PATCH http://localhost:8000/api/article/1/ \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title"}'
# Delete article
curl -X DELETE http://localhost:8000/api/article/1/
# Custom endpoints
curl -X POST http://localhost:8000/api/article/1/publish/
curl http://localhost:8000/api/article/stats/
curl http://localhost:8000/api/article/popular/?limit=5
Next Steps¶
Now that you have CRUD operations set up, let's add authentication in Step 3: Add Authentication.
!!! success "What You've Learned" - ✅ Creating ViewSets for CRUD operations - ✅ Understanding auto-generated endpoints - ✅ Adding query parameters and filtering - ✅ Creating custom endpoints - ✅ Working with pagination - ✅ Handling errors properly - ✅ Customizing responses
See Also¶
- APIViewSet API Reference - Complete API documentation
- Pagination - Advanced pagination options
- ModelUtil - Working with models