Build an AI Content Moderation API
Build a REST API with Django that uses Claude AI to moderate text content.
Introduction
30 Second Summary
Think about how platforms like Discord, Reddit, or Slack handle millions of messages every day. Behind each one sits an automated moderation system making split-second decisions about what content is safe and what crosses the line.
In this project, you will build a content moderation REST API that uses Claude AI to classify text submissions and return structured moderation decisions through authenticated endpoints.
What You'll Build
A Django REST Framework API that accepts text content via authenticated endpoints, sends it to Claude for AI-powered classification, and stores structured moderation results with filtering and pagination.
By the end of this project, you'll have:
- A Django REST Framework API with token authentication and user-scoped data access.
- A service layer that integrates with the Anthropic API to classify content as safe, warning, or violation.
- Filterable, paginated list endpoints following production API patterns.
- Secret Mission: Add a bulk moderation endpoint that accepts multiple texts in a single request and returns all results.
Are there any prerequisites?
You should have basic Python knowledge and some familiarity with Django project structure and basic views.
You will also need a free Anthropic account to get an API key for Claude. The API calls in this project cost approximately $0.05 total.
Set Up the Project
Before you can build API endpoints or integrate AI, you need a solid project foundation. Django projects follow a specific structure with a configuration package, apps for features, and a virtual environment to isolate dependencies.
In this step, you will scaffold a Django project, install all the packages your API needs, and configure the project to use Django REST Framework for building API endpoints.
In this step, get ready to:
- Create a Django project with a virtual environment and all dependencies installed.
- Configure Django REST Framework with token authentication, filtering, and pagination.
- Verify the development server runs successfully.
Create the project folder and install dependencies
- Open your terminal in Cursor by pressing Ctrl+` (macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir moderator-project
cd moderator-project
- Confirm the folder was created by running:
ls
You should see an empty output since the folder is brand new.
A virtual environment keeps your project's packages separate from your system Python. This prevents version conflicts between projects.
- Create a virtual environment by running:
python3 -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
You should see (venv) appear at the start of your terminal prompt.
Windows
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt.
Not seeing (venv) in your prompt?
On macOS, make sure you used source before the path. On Windows, if you see a permissions error, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell first, then try activating again.
help me activate my Python virtual environment
- Install all the project dependencies by running:
pip install Django==6.0.6 djangorestframework==3.17.1 anthropic==0.116.0 python-dotenv==1.2.2 django-filter==25.2
You should see a success message ending with "Successfully installed" followed by a list of packages.
What are these packages?
- Django is the web framework that handles routing, database models, and the project structure.
- djangorestframework (DRF) adds API-specific tools like serializers, viewsets, and authentication.
- anthropic is the official Python SDK for calling Claude's API.
- python-dotenv loads environment variables from a .env file so you can keep secrets out of your code.
- django-filter lets you add query parameter filtering to your API endpoints with minimal code.
Seeing installation errors?
Make sure (venv) appears in your prompt. If you see permission errors, you may be installing into your system Python instead of the virtual environment.
help me fix pip install errors in my virtual environment
- Scaffold the Django project by running:
django-admin startproject moderator .
What does the dot do?
The . at the end tells Django to create the project files in your current folder instead of nesting them inside an extra directory. This keeps your project structure flat and clean.
- Create the moderation app by running:
python manage.py startapp moderation
- Confirm the project structure was created by running:
ls
You should see manage.py, a moderator/ folder, a moderation/ folder, and a venv/ folder in the output.
- Open this project folder in Cursor by pressing Cmd+O (macOS) or Ctrl+O (Windows).
- Select the moderator-project folder on your Desktop, then click Open.
You should see the project files appear in Cursor's Explorer sidebar on the left.
- Create a requirements.txt file in the project root by right-clicking in the Explorer sidebar and selecting New File.
- Name the file requirements.txt and add this content:
Django==6.0.6
djangorestframework==3.17.1
anthropic==0.116.0
python-dotenv==1.2.2
django-filter==25.2
Why create a requirements.txt?
This file documents the exact package versions your project depends on. Anyone else (or your future self) can recreate the environment by running pip install -r requirements.txt.
- Save requirements.txt.
Configure Django settings
Django generated a default settings.py file, but it does not know about DRF, token authentication, or filtering yet. You need to register your new packages and configure their defaults.
- In the Explorer sidebar, expand the moderator/ folder and click on settings.py to open it.
- Select all the existing content and replace it with the following:
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "django-insecure-change-me-in-production"
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework.authtoken",
"django_filters",
"moderation",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "moderator.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "moderator.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.OrderingFilter",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}
What did you add to Django's defaults?
The key additions that differ from a fresh Django project are:
- load_dotenv() at the top loads environment variables from your .env file so your API key is available without hardcoding it.
- Four new entries in INSTALLED_APPS: rest_framework for DRF, rest_framework.authtoken for token auth, django_filters for filtering, and moderation for your app.
- The REST_FRAMEWORK dict sets project-wide defaults. Every API endpoint will require a valid token, support filtering and ordering, and paginate results at 10 items per page.
- Save settings.py.
Seeing a ModuleNotFoundError later?
If Django cannot find rest_framework or django_filters, your terminal session may have lost the virtual environment activation. Check that (venv) still appears in your prompt. If not, re-activate it.
help me fix ModuleNotFoundError in my Django project
✔️ Awesome, I've got everything!
Great. Make sure you have saved settings.py before moving on.
ⓧ I'd like to double check the full code
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "django-insecure-change-me-in-production"
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework.authtoken",
"django_filters",
"moderation",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "moderator.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "moderator.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.OrderingFilter",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}
Set up environment variables and verify the server
Your API key needs to live outside of your code so it never gets committed to version control. A .env file is the standard place to store secrets during local development.
- Create a new file in the project root called .env by right-clicking in the Explorer sidebar and selecting New File.
- Add your Anthropic API key:
ANTHROPIC_API_KEY=your-api-key-here
- Replace your-api-key-here with the actual API key you obtained from the Anthropic console.
- Save .env.
Why not use a variable placeholder for the API key?
API keys are secrets. You paste your real key directly into the .env file on your machine. Never commit this file to Git or share it.
Now you need to run Django's migrations to create the database tables. The rest_framework.authtoken app creates a token table that your API will use for authentication.
- Run database migrations by running this command in your terminal:
python manage.py migrate
You should see a list of migrations being applied, ending with lines like OK next to each migration name.
Seeing an ImportError on migrate?
If you see an error about dotenv or rest_framework, make sure your virtual environment is still active. Check for (venv) in your prompt.
help me fix my Django migration errors
- Create a superuser account by running:
python manage.py createsuperuser
What will this command ask?
Django will prompt you for a username, email address, and password. The email is optional (you can press Enter to skip it). Choose a simple password you will remember for testing. Django may warn that common passwords are too simple. Type y to bypass the warning.
- Start the Django development server by running:
python manage.py runserver
You should see output containing Starting development server at http://127.0.0.1:8000/ with no error messages.
- Open your browser and navigate to http://127.0.0.1:8000/.
You should see the Django "Congratulations" welcome page with a rocket icon, confirming your project is running.
- Navigate to http://127.0.0.1:8000/admin/ in your browser.
You should see the Django admin login page. This confirms migrations ran correctly and the authentication system is working.
Server not starting?
If you see Error: That port is already in use, another process is using port 8000. Try running python manage.py runserver 8001 instead and update the URL in your browser accordingly.
help me start my Django development server
Your project foundation is solid. Next up, you will design the database models and serializers that define how moderation data is structured and exposed through the API.
Design Models and Serializers
Your Django project is running and configured with DRF, token auth, and filtering. Now it needs a data layer that stores moderation decisions and controls what data flows in and out of the API.
Every REST API needs two things: a database model defining what gets stored, and serializers defining how that data is represented in JSON. In this step, you will define the ModerationRequest model and create serializers that shape the API's input and output.
In this step, get ready to:
- Define the ModerationRequest model and run migrations.
- Create serializers to control API input and output.
- Verify the model works by creating a record in the Django shell.
Define the ModerationRequest model
The ModerationRequest model stores every piece of content submitted for moderation along with the AI's decision. Each record links to a user, holds the submitted text, and records the classification result.
- In Cursor, open moderation/models.py and replace its contents with the following code:
from django.db import models
from django.contrib.auth.models import User
class ModerationRequest(models.Model):
# Possible categories the AI can assign
CATEGORY_CHOICES = [
("safe", "Safe"),
("warning", "Warning"),
("violation", "Violation"),
("error", "Error"),
]
# Link each moderation request to the user who submitted it
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="moderation_requests")
# The original text content submitted for moderation
content = models.TextField()
# AI classification result
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
# Numeric confidence from 0.0 (no concern) to 1.0 (severe)
severity_score = models.FloatField(default=0.0)
# AI explanation of why it chose this category
reasoning = models.TextField(blank=True)
# Timestamp for when the request was submitted
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return f"{self.user.username} - {self.category} ({self.created_at:%Y-%m-%d %H:%M})"
What does this model do?
user is a foreign key linking every moderation request to the authenticated user who submitted it. When a user is deleted, on_delete=models.CASCADE removes their moderation history too.
category uses choices to restrict values to a predefined set. The ordering = ["-created_at"] in Meta means queries return newest results first by default.
- Save moderation/models.py.
- Generate and apply the migration by running these commands in your terminal:
python manage.py makemigrations moderation
python manage.py migrate
- You should see output showing Create model ModerationRequest from makemigrations, followed by Applying moderation.0001_initial... OK from migrate.
Seeing a ModuleNotFoundError or No installed app?
Make sure "moderation" is listed in INSTALLED_APPS in moderator/settings.py. Also confirm your virtual environment is active before running manage.py commands.
help me fix my Django migration error
Now register the model in the admin panel so you can inspect records later.
- Open moderation/admin.py and replace its contents with the following:
from django.contrib import admin
from .models import ModerationRequest
@admin.register(ModerationRequest)
class ModerationRequestAdmin(admin.ModelAdmin):
# Columns visible in the admin list view
list_display = ["user", "category", "severity_score", "created_at"]
# Sidebar filters for quick searching
list_filter = ["category", "created_at"]
# Fields searchable via the admin search bar
search_fields = ["content", "reasoning"]
# Prevent editing the auto-set timestamp
readonly_fields = ["created_at"]
What does the admin registration do?
The @admin.register decorator connects your model to Django's built-in admin interface. You can browse, search, and filter moderation records at /admin/ without writing any extra views.
- Save moderation/admin.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved both moderation/models.py and moderation/admin.py.
ⓧ I'd like to double check the full code
Here is the complete moderation/models.py:
from django.db import models
from django.contrib.auth.models import User
class ModerationRequest(models.Model):
CATEGORY_CHOICES = [
("safe", "Safe"),
("warning", "Warning"),
("violation", "Violation"),
("error", "Error"),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="moderation_requests")
content = models.TextField()
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
severity_score = models.FloatField(default=0.0)
reasoning = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return f"{self.user.username} - {self.category} ({self.created_at:%Y-%m-%d %H:%M})"
And the complete moderation/admin.py:
from django.contrib import admin
from .models import ModerationRequest
@admin.register(ModerationRequest)
class ModerationRequestAdmin(admin.ModelAdmin):
list_display = ["user", "category", "severity_score", "created_at"]
list_filter = ["category", "created_at"]
search_fields = ["content", "reasoning"]
readonly_fields = ["created_at"]
Create the output serializer
In Django REST Framework, serializers translate between Python model instances and JSON. The output serializer controls what fields appear in API responses when a client fetches moderation results.
- Create a new file called moderation/serializers.py and add the following code:
from rest_framework import serializers
from .models import ModerationRequest
class ModerationRequestSerializer(serializers.ModelSerializer):
# Display the username string instead of the user's numeric ID
user = serializers.StringRelatedField(read_only=True)
class Meta:
model = ModerationRequest
fields = ["id", "user", "content", "category", "severity_score", "reasoning", "created_at"]
# These fields are set by the system, not by the user
read_only_fields = ["id", "user", "category", "severity_score", "reasoning", "created_at"]
What does this serializer do?
ModelSerializer automatically generates fields based on the model. The read_only_fields list prevents clients from setting values that only the server should control (like the AI's category decision or the timestamp).
StringRelatedField calls the User model's __str__ method, so the JSON shows a username like "admin" instead of a numeric foreign key ID.
- Save moderation/serializers.py.
Create the input serializer
The output serializer handles responses, but you also need a dedicated serializer for incoming requests. When a user submits text for moderation, the only field they send is the content itself.
- In moderation/serializers.py, add the following class below ModerationRequestSerializer:
class ModerationSubmitSerializer(serializers.Serializer):
# Users submit only the text they want moderated (1-5000 chars)
content = serializers.CharField(min_length=1, max_length=5000)
Why a separate input serializer?
Separating input from output is a production best practice. The submit serializer validates what the user sends (just the text content with length limits). The output serializer controls what the API returns (the full moderation result with all fields).
This pattern prevents users from injecting values into fields they should not control, like category or severity_score.
- Save moderation/serializers.py.
- Verify the model works by opening the Django shell:
python manage.py shell
- In the shell, create a test ModerationRequest instance by running:
from django.contrib.auth.models import User
from moderation.models import ModerationRequest
# Get the superuser you created in Step 1
user = User.objects.first()
# Create a test moderation request
request = ModerationRequest.objects.create(
user=user,
content="Test content for moderation",
category="safe",
severity_score=0.1,
reasoning="This is a test entry."
)
print(request)
print(f"ID: {request.id}, Category: {request.category}")
- You should see output showing your username, the category, and a timestamp. Type exit() to leave the shell.
Getting an error in the shell?
If you see no such table, you missed the python manage.py migrate step. Run it now and try the shell commands again.
If User.objects.first() returns None, you have not created a superuser yet. Exit the shell and run python manage.py createsuperuser first.
help me fix my Django shell error
✔️ Awesome, I've got everything!
Great. Make sure you have saved moderation/serializers.py with both serializer classes.
ⓧ I'd like to double check the full code
Here is the complete moderation/serializers.py:
from rest_framework import serializers
from .models import ModerationRequest
class ModerationRequestSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(read_only=True)
class Meta:
model = ModerationRequest
fields = ["id", "user", "content", "category", "severity_score", "reasoning", "created_at"]
read_only_fields = ["id", "user", "category", "severity_score", "reasoning", "created_at"]
class ModerationSubmitSerializer(serializers.Serializer):
content = serializers.CharField(min_length=1, max_length=5000)
Your data layer is ready. Next up, you will build the service layer that calls Claude to classify content and returns structured moderation decisions.
Build the AI Service Layer
Your models and serializers are ready to store and format moderation results. Now you need the engine that actually decides whether content is safe, a warning, or a violation.
In this step, you will create a dedicated service module that talks to the Anthropic API. This module sends user-submitted text to Claude, receives a structured classification, and returns it as a Python dictionary your views can save directly.
In this step, get ready to:
- Create a service module with a function that calls the Anthropic Messages API.
- Parse AI responses as JSON and handle errors gracefully.
- Test the service from the Django shell with sample text.
Create the service module
Production codebases keep external API calls in their own module instead of mixing them into views. This separation makes the logic reusable, testable, and easy to swap out later.
- In the moderation app folder inside Cursor, create a new file called services.py.
- Add the following code to moderation/services.py:
import json
import os
from anthropic import Anthropic
def get_client():
# Create and return an Anthropic client using the API key from environment
return Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
def moderate_content(text: str) -> dict:
client = get_client()
# System prompt instructs Claude to return structured JSON classification
system_prompt = (
"You are a content moderation system. Classify the following text into one of "
"three categories: safe, warning, or violation.\n\n"
"Respond ONLY with valid JSON in this exact format:\n"
'{"category": "safe|warning|violation", "severity_score": 0.0-1.0, "reasoning": "brief explanation"}\n\n'
"Guidelines:\n"
"- safe: Normal content with no issues (severity_score 0.0-0.3)\n"
"- warning: Content that is borderline or mildly inappropriate (severity_score 0.3-0.7)\n"
"- violation: Content that is clearly harmful, hateful, or dangerous (severity_score 0.7-1.0)\n"
)
try:
# Call the Anthropic Messages API with the user's text
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=256,
system=system_prompt,
messages=[
{"role": "user", "content": text},
],
)
response_text = message.content[0].text
result = json.loads(response_text)
# Normalize unexpected categories to 'error'
if result.get("category") not in ("safe", "warning", "violation"):
result["category"] = "error"
# Clamp severity_score between 0.0 and 1.0
result["severity_score"] = max(0.0, min(1.0, float(result.get("severity_score", 0.0))))
return result
except Exception:
# Return a safe fallback if anything goes wrong
return {
"category": "error",
"severity_score": 0.0,
"reasoning": "Moderation service encountered an error.",
}
What does this code do?
get_client() creates an Anthropic client using the ANTHROPIC_API_KEY you stored in .env earlier. The load_dotenv() call in settings.py already loaded that value into the environment.
moderate_content() sends the text to claude-haiku-4-5-20251001 with a system prompt that forces a JSON response. It then parses that JSON, validates the category, clamps the severity score to a 0.0-1.0 range, and returns the result as a dictionary.
If anything fails (network error, invalid JSON, API outage), the except Exception block returns a fallback dictionary with category: "error". This prevents your API from crashing when the AI service is unavailable.
Why a separate service module?
Keeping external API logic in services.py instead of your views means you can test the AI integration in isolation. You can also swap to a different AI provider later by changing one file instead of rewriting every view.
This is called the service layer pattern. It is how production backends handle external integrations.
- Save moderation/services.py.
Seeing an import error for anthropic?
Make sure your virtual environment is active (you should see the environment name in parentheses in your terminal prompt). If the import still fails, confirm you installed the Anthropic SDK by checking your requirements.txt includes anthropic and re-run pip install -r requirements.txt.
help me fix an anthropic import error in my Django project
Test the service from the Django shell
Before wiring this into your API views, you can call moderate_content() directly to confirm it talks to Claude and returns the expected dictionary structure.
- Open the Django interactive shell by running this command:
python manage.py shell
- Import and call the service function by running these lines inside the shell:
from moderation.services import moderate_content
result = moderate_content("I love puppies")
print(result)
What should I see?
You should see a dictionary printed with three keys: category (likely "safe"), severity_score (a low number like 0.1), and reasoning (a brief explanation from Claude). The exact wording of the reasoning will vary each time, but the category should consistently be "safe" for this input.
- Exit the shell by typing exit() and pressing Enter.
Getting an error instead of a result?
- If you see a dictionary with category: "error", your API key is likely missing or invalid. Double-check that your .env file contains your actual Anthropic API key (not the placeholder your-api-key-here).
- If you see ModuleNotFoundError, make sure you started the shell from the same directory as manage.py and that your virtual environment is active.
help me debug my moderate_content function in the Django shell
✔️ Awesome, I've got everything!
Great. Double check you have saved moderation/services.py and that your shell test returned a valid moderation result.
ⓧ I'd like to double check the full code
import json
import os
from anthropic import Anthropic
def get_client():
return Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
def moderate_content(text: str) -> dict:
client = get_client()
system_prompt = (
"You are a content moderation system. Classify the following text into one of "
"three categories: safe, warning, or violation.\n\n"
"Respond ONLY with valid JSON in this exact format:\n"
'{"category": "safe|warning|violation", "severity_score": 0.0-1.0, "reasoning": "brief explanation"}\n\n'
"Guidelines:\n"
"- safe: Normal content with no issues (severity_score 0.0-0.3)\n"
"- warning: Content that is borderline or mildly inappropriate (severity_score 0.3-0.7)\n"
"- violation: Content that is clearly harmful, hateful, or dangerous (severity_score 0.7-1.0)\n"
)
try:
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=256,
system=system_prompt,
messages=[
{"role": "user", "content": text},
],
)
response_text = message.content[0].text
result = json.loads(response_text)
if result.get("category") not in ("safe", "warning", "violation"):
result["category"] = "error"
result["severity_score"] = max(0.0, min(1.0, float(result.get("severity_score", 0.0))))
return result
except Exception:
return {
"category": "error",
"severity_score": 0.0,
"reasoning": "Moderation service encountered an error.",
}
Your AI service layer is working. Next up, you will wire it into proper API endpoints so users can submit content through HTTP requests and get moderation results back.
Wire Up ViewSets and URLs
Your service layer can classify content, and your serializers know how to validate and format that data. The missing piece is the layer that ties everything together and exposes it over HTTP.
In this step, you'll create a ViewSet that handles incoming requests, calls your AI service, saves the result, and returns it to the user. Then you'll register URL routes so the API is actually reachable.
In this step, get ready to:
- Create a ModerationViewSet that scopes data to the authenticated user.
- Override the create method to call the AI service and save structured results.
- Register routes with a DefaultRouter and wire up the project URLs.
Create the ModerationViewSet
A ModelViewSet gives you list, retrieve, create, update, and delete actions out of the box. You'll restrict the allowed HTTP methods and override get_queryset() so each user only sees their own moderation history.
- Create a new file called views.py inside the moderation folder (it may already exist as an empty file from startapp).
- Add the following code to define the ViewSet with user-scoped queries:
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import ModerationRequest
from .serializers import (
ModerationRequestSerializer,
ModerationSubmitSerializer,
)
from .services import moderate_content
class ModerationViewSet(viewsets.ModelViewSet):
# Use the full serializer for reading moderation results
serializer_class = ModerationRequestSerializer
# Allow filtering by category (e.g. ?category=safe)
filterset_fields = ["category"]
# Allow ordering by date or severity
ordering_fields = ["created_at", "severity_score"]
ordering = ["-created_at"]
# Only allow GET and POST (no PUT/PATCH/DELETE)
http_method_names = ["get", "post", "head", "options"]
def get_queryset(self):
# Each user can only see their own moderation requests
return ModerationRequest.objects.filter(user=self.request.user)
What does this code do?
The class attributes configure DRF's built-in behaviors. filterset_fields enables query parameter filtering so clients can request only results of a specific category.
The http_method_names list locks down the endpoint to read and create operations. Users cannot update or delete moderation records once created.
The get_queryset() override is the key security boundary. It ensures User A cannot see User B's moderation results, even if they guess the ID.
- Save moderation/views.py.
- You won't see a change yet. The next chunk adds the custom create logic that calls your AI service.
Override the create method
The default create() method on a ModelViewSet simply saves whatever the client sends. Your API needs custom logic: validate just the content field, send it to Claude for classification, then save the full result.
- In moderation/views.py, add the following create method inside the ModerationViewSet class, below get_queryset():
def create(self, request, *args, **kwargs):
# Validate the incoming content using the submit serializer
submit_serializer = ModerationSubmitSerializer(data=request.data)
submit_serializer.is_valid(raise_exception=True)
# Extract the text and send it to the AI moderation service
content = submit_serializer.validated_data["content"]
result = moderate_content(content)
# Save the full moderation result to the database
moderation = ModerationRequest.objects.create(
user=request.user,
content=content,
category=result["category"],
severity_score=result["severity_score"],
reasoning=result["reasoning"],
)
# Return the saved record using the read serializer
output_serializer = ModerationRequestSerializer(moderation)
return Response(output_serializer.data, status=status.HTTP_201_CREATED)
What does this code do?
This method uses two different serializers. ModerationSubmitSerializer validates the inbound POST body (just the content field). ModerationRequestSerializer formats the saved database record for the response.
Between validation and response, the method calls moderate_content() from your service layer. This keeps the view thin. All the AI logic lives in services.py where it can be tested or swapped independently.
- Save moderation/views.py.
- You won't see a change yet. The next chunk registers the URL routes so the endpoint becomes reachable.
Register routes and wire up project URLs
DRF's DefaultRouter automatically generates URL patterns for all the standard ViewSet actions (list, create, retrieve). You also need a token endpoint so users can authenticate.
- Create a new file called urls.py inside the moderation folder.
- Add the following code to register the ViewSet with a router:
from rest_framework.routers import DefaultRouter
from .views import ModerationViewSet
router = DefaultRouter()
router.register(r"moderations", ModerationViewSet, basename="moderation")
urlpatterns = router.urls
What does this code do?
The router registers your ViewSet at the moderations/ path. It automatically creates routes like moderations/ (list and create) and moderations/<id>/ (retrieve).
The basename parameter is required because your ViewSet defines a custom get_queryset() instead of setting queryset directly. DRF uses it to generate URL names.
- Save moderation/urls.py.
- Now open moderator/urls.py (in the project-level moderator folder) and replace its contents with this:
from django.contrib import admin
from django.urls import path, include
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
path("admin/", admin.site.urls),
# Token endpoint for users to authenticate
path("api/token/", obtain_auth_token, name="api_token_auth"),
# Include all moderation app URLs under /api/
path("api/", include("moderation.urls")),
]
What does this code do?
The api/token/ endpoint is DRF's built-in view for exchanging a username and password for an auth token. Clients POST their credentials here to get a token they'll include in all future requests.
The include("moderation.urls") line mounts your router's generated URLs under /api/. Combined with the router prefix, your moderation endpoints live at /api/moderations/.
- Save moderator/urls.py.
- Return to your terminal and restart the dev server by running:
python manage.py runserver
- The server should start without errors. Open http://127.0.0.1:8000/api/ in your browser to confirm the browsable API root now shows a moderations link.
Seeing an import error or the server won't start?
- Check that moderation/views.py imports match exactly: ModerationRequestSerializer and ModerationSubmitSerializer from .serializers.
- Verify moderation/urls.py exists in the moderation app folder (not the moderator project folder).
- Make sure you're editing moderator/urls.py (the project-level file), not moderation/urls.py (the app-level file).
Help me debug my Django URL configuration error.
Now verify the full flow works by obtaining a token and submitting content. Open a new terminal tab and run this command to get your auth token:
curl -X POST http://127.0.0.1:8000/api/token/ -d "username=admin&password=[[ADMIN_PASSWORD="your superuser password"]]"
- You should see a JSON response containing a token field. Copy that token value.
- Submit a moderation request using the token by running:
curl -X POST http://127.0.0.1:8000/api/moderations/ -H "Authorization: Token [[AUTH_TOKEN="your token from the previous response"]]" -H "Content-Type: application/json" -d "{\"content\": \"hello world\"}"
- You should see a 201 response containing the full moderation result with category, severity_score, and reasoning fields populated by Claude.
What should I see?
The response should look something like this: {"id": 1, "user": "admin", "content": "hello world", "category": "safe", "severity_score": 0.05, "reasoning": "...", "created_at": "..."}. The exact severity_score and reasoning will vary since Claude generates them dynamically.
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files before moving on.
ⓧ I'd like to double check the full code
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import ModerationRequest
from .serializers import (
ModerationRequestSerializer,
ModerationSubmitSerializer,
)
from .services import moderate_content
class ModerationViewSet(viewsets.ModelViewSet):
serializer_class = ModerationRequestSerializer
filterset_fields = ["category"]
ordering_fields = ["created_at", "severity_score"]
ordering = ["-created_at"]
http_method_names = ["get", "post", "head", "options"]
def get_queryset(self):
return ModerationRequest.objects.filter(user=self.request.user)
def create(self, request, *args, **kwargs):
submit_serializer = ModerationSubmitSerializer(data=request.data)
submit_serializer.is_valid(raise_exception=True)
content = submit_serializer.validated_data["content"]
result = moderate_content(content)
moderation = ModerationRequest.objects.create(
user=request.user,
content=content,
category=result["category"],
severity_score=result["severity_score"],
reasoning=result["reasoning"],
)
output_serializer = ModerationRequestSerializer(moderation)
return Response(output_serializer.data, status=status.HTTP_201_CREATED)
from rest_framework.routers import DefaultRouter
from .views import ModerationViewSet
router = DefaultRouter()
router.register(r"moderations", ModerationViewSet, basename="moderation")
urlpatterns = router.urls
from django.contrib import admin
from django.urls import path, include
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
path("admin/", admin.site.urls),
path("api/token/", obtain_auth_token, name="api_token_auth"),
path("api/", include("moderation.urls")),
]
Your API is live and returning AI-powered moderation decisions. Next up, you'll test the full flow with multiple users and confirm that filtering, pagination, and user isolation all work correctly.
Test the Full Flow
You have a fully built API with models, serializers, a service layer, and authenticated endpoints. Now it's time to test everything end-to-end the way a real client application would interact with your API.
In this step, you'll obtain an auth token using curl, submit content for AI moderation, and verify that filtering, pagination, and user isolation all work correctly.
In this step, get ready to:
- Obtain an auth token and submit content for AI moderation.
- Verify filtering and pagination on list endpoints.
- Confirm permission isolation between users.
Obtain a token and submit content for moderation
Every request to your API requires a valid auth token in the header. The /api/token/ endpoint exchanges a username and password for a token string you'll include in subsequent requests.
- Open a new terminal window while your dev server continues running in the existing one.
- Activate your virtual environment in the new terminal.
- Obtain your superuser's auth token by running this command:
curl -X POST http://127.0.0.1:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username": "your-superuser-username", "password": "your-password-here"}'
What does this command do?
This sends a POST request to the token endpoint with your superuser credentials. The server validates the credentials and returns a JSON response containing your token.
Replace your-superuser-username and your-password-here with the credentials you chose when you ran createsuperuser in Step 1.
- You should see a JSON response like this:
{"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"}
Your token value will be different. Copy it somewhere handy because you'll use it in every following request.
Getting a 400 Bad Request?
- Double check that your username and password exactly match what you entered during createsuperuser.
- Make sure your dev server is still running in the other terminal.
- Verify the URL ends with a trailing slash: /api/token/.
Help me debug my token request
Now submit a clearly safe piece of content for moderation.
- Submit safe content by running this command (replace your-token-here with the token you just received):
curl -X POST http://127.0.0.1:8000/api/moderations/ \
-H "Authorization: Token your-token-here" \
-H "Content-Type: application/json" \
-d '{"content": "I love puppies and sunshine"}'
- You should see a 201 response with a JSON body containing category, severity_score, and reasoning fields.
- The category should come back as safe.
What is happening behind the scenes?
Your API validates the token, passes the submitted text to moderate_content() in your service layer, which calls the Anthropic API.
Claude classifies the text, returns a JSON response with category, severity, and reasoning, and your view saves everything to the database before returning it to the caller.
- Submit borderline content by running:
curl -X POST http://127.0.0.1:8000/api/moderations/ \
-H "Authorization: Token your-token-here" \
-H "Content-Type: application/json" \
-d '{"content": "This product is absolute garbage and the developers should be ashamed"}'
- This might come back as warning since it's aggressive but not hateful.
- Submit clearly violating content by running:
curl -X POST http://127.0.0.1:8000/api/moderations/ \
-H "Authorization: Token your-token-here" \
-H "Content-Type: application/json" \
-d '{"content": "I hate everyone in this group and wish them harm"}'
- This should return violation with a high severity_score.
AI classifications may vary
Claude's responses are non-deterministic. The exact severity_score and reasoning will differ each time. What matters is that the category aligns with the content's intent.
Getting a 401 Unauthorized?
- Make sure the header reads Authorization: Token your-token-here with a capital T in Token.
- Check that there's a space between Token and the actual token string.
- Verify you copied the full token with no trailing spaces.
Help me fix my authorization header
Test filtering and pagination
Production APIs need to support efficient querying. Your API is configured with django-filter for category filtering and PageNumberPagination for paginated results.
- Filter your moderation results to show only safe content by running:
curl http://127.0.0.1:8000/api/moderations/?category=safe \
-H "Authorization: Token your-token-here"
- You should see only results where category is safe in the response.
What does the paginated response look like?
The response body is no longer a plain list of objects. DRF's PageNumberPagination wraps results in a structure with four keys:
- count - the total number of matching records.
- next - URL of the next page, or null if this is the last page.
- previous - URL of the previous page, or null if this is the first page.
- results - the actual list of serialized moderation requests for this page.
- Retrieve all your moderation results (unfiltered) to confirm the pagination structure by running:
curl http://127.0.0.1:8000/api/moderations/ \
-H "Authorization: Token your-token-here"
- Confirm the response JSON contains count, next, previous, and results keys at the top level.
- The count should be 3 (matching the three submissions you made).
- The next and previous fields should both be null since you have fewer than 10 results (your page size).
Not seeing the pagination keys?
- Check that DEFAULT_PAGINATION_CLASS and PAGE_SIZE are both set in the REST_FRAMEWORK dict in settings.py.
- Make sure you restarted the dev server after making any settings changes.
Help me fix my pagination setup
Confirm permission isolation between users
A critical security requirement is that users can only see their own moderation history. You'll create a second user, submit content as that user, and prove the first user cannot see it.
- Open the Django shell by running:
python manage.py shell
- Create a second test user by running these commands in the shell:
from django.contrib.auth.models import User
# Create a second user for isolation testing
User.objects.create_user(username='testuser2', password='testpass123')
exit()
Why create a user via the shell?
Your API doesn't have a registration endpoint (that's out of scope for this project). The Django shell gives you direct access to create users programmatically, which is common during development and testing.
- Get the second user's auth token by running:
curl -X POST http://127.0.0.1:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username": "testuser2", "password": "testpass123"}'
- Copy the token from the response.
- Submit content as the second user by running (replace second-user-token-here with the token you just received):
curl -X POST http://127.0.0.1:8000/api/moderations/ \
-H "Authorization: Token second-user-token-here" \
-H "Content-Type: application/json" \
-d '{"content": "This is a test from user two"}'
- Now fetch the moderation list as your original superuser (using your first token):
curl http://127.0.0.1:8000/api/moderations/ \
-H "Authorization: Token your-token-here"
- Verify that the results only contain your superuser's three original submissions.
- The second user's submission ("This is a test from user two") should NOT appear in this list.
How does user isolation work?
The get_queryset() method in your ModerationViewSet filters all queries by request.user. This means every list or retrieve call automatically scopes results to the authenticated user.
This is the standard DRF pattern for multi-tenant data access. No matter what query parameters a user sends, they can never access another user's records.
- Fetch the moderation list as the second user to confirm they only see their own submission:
curl http://127.0.0.1:8000/api/moderations/ \
-H "Authorization: Token second-user-token-here"
- The count should be 1, and the only result should be the "This is a test from user two" submission.
Seeing the other user's data?
- Confirm you're using the correct token for each user in the Authorization header.
- Check that get_queryset() in moderation/views.py filters by user=self.request.user.
- Make sure you didn't accidentally paste the same token in both commands.
Help me debug user isolation in my API
Your API is fully verified. Authentication, moderation, filtering, pagination, and user isolation are all working as expected. You've built a production-patterned backend service.
Secret mission
Add a Bulk Moderation Endpoint
Real moderation systems process hundreds of messages at once. Can you extend your API to accept multiple texts in a single request and return all moderation decisions together?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally with no cloud infrastructure. The Anthropic API only charges per request, so there are no ongoing costs when your development server is stopped.
Resources you used:
- Your moderator project folder on the Desktop (contains all project files, the virtual environment, and the SQLite database).
- Your Anthropic API key (stored in .env and active on your Anthropic account).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project soon.
- Your project files, database, and virtual environment stay on your Desktop.
- Your Anthropic API key remains active but costs nothing while idle.
- The Django development server only runs when you start it manually.
Pause - I'll come back to this later
Stop any running processes to free up system resources. Your files and API key stay intact for when you return.
- If the Django development server is still running, press Ctrl+C in the terminal to stop it.
- Close any terminal sessions associated with the project.
- Your project folder, virtual environment, and API key all remain ready for next time.
Delete - I don't want to use this again
Remove all project resources and revoke access to start fresh.
- Stop the Django development server by pressing Ctrl+C if it is still running.
- Delete your project folder by running the command for your operating system.
macOS
rm -rf ~/Desktop/moderator
Windows
rmdir /s /q %USERPROFILE%\Desktop\moderator
- Revoke your Anthropic API key to prevent unauthorized use.
- Sign in to the Anthropic Console at console.anthropic.com.
- Navigate to Settings then API Keys.
- Find the key you created for this project and delete it.
Nice Work!
Nice Work!
Nice work! You just built a production-patterned content moderation API powered by AI. This is the exact kind of backend service that platforms like Discord, Reddit, and Slack rely on to keep their communities safe.
You've learned how to:
- Build a Django REST Framework API with Token Authentication and user-scoped data access via queryset filtering.
- Design a service layer that integrates with an external AI API (Anthropic Claude) and parses structured JSON responses.
- Add filtering with django-filter and pagination for production-ready list endpoints.
- Created a custom bulk action endpoint using the @action decorator to handle batch moderation requests (Secret Mission).
Ready to quiz yourself?