Reusable Django REST API framework with auto-discovery and bearer token authentication
Project description
django-directory-api
Reusable Django REST API framework with auto-discovery and bearer token authentication.
Features
- 🔐 Bearer Token Authentication - Secure API access with per-user tokens
- 🔌 Auto-Discovery - Automatically discovers and registers API routers from
api.pyfiles - 📚 Django Shinobi - Built on Django Shinobi (Django Ninja fork) for type-safe APIs
- 🤖 LLM-Optimized - Rich OpenAPI documentation designed for AI agent consumption
- 🎯 Zero Config - Just create an
api.pyfile and start building
Installation
pip install django-directory-api
Quick Start
1. Add to INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
# ...
"django_directory_api", # Must come before apps that define API endpoints
# ...
]
2. Include API URLs
# urls.py
from django_directory_api import api
urlpatterns = [
path("api/", api.urls),
# ...
]
3. Create API Endpoints
Create an api.py file in any Django app:
# myapp/api.py
from ninja import Router
from .models import MyModel
router = Router(tags=["My App"])
@router.get("/items/")
def list_items(request):
return {"items": list(MyModel.objects.values())}
That's it! The router is automatically discovered and registered.
Authentication
Creating API Tokens
- Log into Django admin
- Navigate to "API Tokens"
- Click "Add API Token"
- Give it a name (e.g., "Production Bot")
- Copy the token value (shown only once)
Using Tokens
curl -H "Authorization: Bearer <your-token>" \
https://example.com/api/items/
import requests
headers = {"Authorization": "Bearer <your-token>"}
response = requests.get("https://example.com/api/items/", headers=headers)
Auto-Discovery
The package automatically discovers api.py files in all installed Django apps:
- ✅ Looks for
routerattribute (single router) - ✅ Looks for
routersattribute (list of routers) - ✅ Skips apps without
api.pyfiles - ✅ No explicit registration required
Example with Multiple Routers
# myapp/api.py
from ninja import Router
public_router = Router(tags=["Public"])
admin_router = Router(tags=["Admin"])
@public_router.get("/public/")
def public_endpoint(request):
return {"message": "Hello world"}
@admin_router.get("/admin/")
def admin_endpoint(request):
return {"message": "Admin only"}
# Export multiple routers
routers = [public_router, admin_router]
Advanced Patterns
Production-Ready CRUD API
Here's a complete example showing best practices for a production API:
# myapp/api.py
from django.shortcuts import get_object_or_404
from ninja import Router
from django_directory_api.schemas import PaginatedResponse
from .models import Article
from .schemas import ArticleListSchema, ArticleDetailSchema, ArticleCreateSchema, ArticleUpdateSchema
router = Router(tags=["Articles"])
@router.get("/articles/", response=PaginatedResponse[ArticleListSchema])
def list_articles(request, page: int = 1, page_size: int = 50, is_published: bool | None = None):
"""List articles with pagination and filtering."""
queryset = Article.objects.all()
if is_published is not None:
queryset = queryset.filter(is_published=is_published)
# Enforce max page size
page_size = min(page_size, 100)
# Calculate pagination
total = queryset.count()
offset = (page - 1) * page_size
items = list(queryset[offset:offset + page_size])
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size,
}
@router.get("/articles/{slug}/", response=ArticleDetailSchema)
def get_article(request, slug: str):
"""Get detailed information for a specific article."""
return get_object_or_404(Article, slug=slug)
@router.post("/articles/", response={201: ArticleDetailSchema})
def create_article(request, data: ArticleCreateSchema):
"""Create a new article."""
article = Article.objects.create(**data.dict(exclude_unset=True))
return 201, article
@router.patch("/articles/{slug}/", response=ArticleDetailSchema)
def update_article(request, slug: str, data: ArticleUpdateSchema):
"""Update an article (partial update)."""
article = get_object_or_404(Article, slug=slug)
update_data = data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(article, field, value)
article.save()
return article
@router.delete("/articles/{slug}/", response={204: None})
def delete_article(request, slug: str):
"""Delete an article permanently."""
article = get_object_or_404(Article, slug=slug)
article.delete()
return 204, None
Nested Resources
Handle parent-child relationships elegantly:
# myapp/api.py
from ninja import Router
router = Router(tags=["Articles"])
@router.get("/articles/{article_slug}/comments/", response=list[CommentSchema])
def list_comments(request, article_slug: str):
"""Get all comments for an article."""
article = get_object_or_404(Article, slug=article_slug)
return list(article.comments.all().order_by("-created_at"))
@router.post("/articles/{article_slug}/comments/", response={201: CommentSchema})
def create_comment(request, article_slug: str, data: CommentCreateSchema):
"""Add a comment to an article."""
article = get_object_or_404(Article, slug=article_slug)
comment = Comment.objects.create(article=article, **data.dict(exclude_unset=True))
return 201, comment
Schema Best Practices
Organizing Schemas
Create a separate schemas.py file in your app:
# myapp/schemas.py
from datetime import datetime
from pydantic import BaseModel, ConfigDict
class ArticleListSchema(BaseModel):
"""Lightweight schema for list views."""
model_config = ConfigDict(from_attributes=True)
slug: str
title: str
excerpt: str
is_published: bool
created_at: datetime
class ArticleDetailSchema(BaseModel):
"""Complete schema with all fields."""
model_config = ConfigDict(from_attributes=True)
slug: str
title: str
content: str
excerpt: str
is_published: bool
author_name: str
created_at: datetime
updated_at: datetime
# Computed properties from Django model
word_count: int
reading_time: int
class ArticleCreateSchema(BaseModel):
"""Schema for creating articles."""
title: str
content: str
excerpt: str | None = None
is_published: bool = False
class ArticleUpdateSchema(BaseModel):
"""Schema for partial updates (all fields optional)."""
title: str | None = None
content: str | None = None
excerpt: str | None = None
is_published: bool | None = None
Using Computed Properties
Django model methods work automatically with from_attributes=True:
# myapp/models.py
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
@property
def word_count(self):
"""Computed property available in API responses."""
return len(self.content.split())
@property
def reading_time(self):
"""Estimated reading time in minutes."""
return max(1, self.word_count // 200)
# myapp/schemas.py
class ArticleDetailSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
title: str
content: str
word_count: int # ← Automatically calls model property
reading_time: int # ← Automatically calls model property
Common Schemas Reference
The package provides reusable schemas in django_directory_api.schemas:
PaginatedResponse[T]
Generic pagination wrapper for list endpoints:
from django_directory_api.schemas import PaginatedResponse
from .schemas import ArticleListSchema
@router.get("/articles/", response=PaginatedResponse[ArticleListSchema])
def list_articles(request, page: int = 1, page_size: int = 50):
# ... pagination logic ...
return {
"items": items, # List of items
"total": total, # Total count
"page": page, # Current page
"page_size": page_size, # Items per page
"pages": total_pages, # Total pages
}
Enums
Standard enums for common patterns:
from django_directory_api.schemas import (
BackfillStatusEnum, # pending, done, error, no_backfill
PublishStatusEnum, # draft, published, archived
ExperienceLevelEnum, # beginner, intermediate, advanced, expert
)
class ArticleSchema(BaseModel):
status: PublishStatusEnum
level: ExperienceLevelEnum
Response Schemas
from django_directory_api.schemas import MessageResponse, ErrorResponse
@router.post("/articles/{slug}/publish/", response=MessageResponse)
def publish_article(request, slug: str):
article = get_object_or_404(Article, slug=slug)
article.is_published = True
article.save()
return {"message": f"Article '{article.title}' published successfully"}
Real-World Examples
Complete implementations you can reference:
Pages API (django-directory-cms)
Full-featured CMS pages API with SEO fields:
- File:
django-directory-cms/src/django_directory_cms/api.py - Schemas:
django-directory-cms/src/django_directory_cms/schemas.py - Features: CRUD operations, SEO management, auto-slug generation
- GitHub: django-directory-cms
Categories API
Complex hierarchical data with nested subpages:
- File:
categories/api.pyin directory-builder - Schemas:
categories/schemas.py - Features: Parent-child relationships, pagination, filtering, nested resources
- Pattern:
/categories/{slug}/subpages/for nested resources
Entities API
Many-to-many relationships and linking:
- File:
entities/api.pyin directory-builder - Schemas:
entities/schemas.py - Features: Link management, relationship endpoints, bulk operations
Testing
Basic Test Pattern
# myapp/tests/test_api.py
from django.test import TestCase
from django_directory_api.models import APIToken
from django.contrib.auth import get_user_model
User = get_user_model()
class ArticleAPITest(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(email="test@example.com", password="test123")
self.token = APIToken.objects.create(user=self.user, name="Test Token")
self.auth_headers = {"HTTP_AUTHORIZATION": f"Bearer {self.token.key}"}
def test_list_articles(self):
"""Test article listing endpoint."""
response = self.client.get("/api/articles/", **self.auth_headers)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("items", data)
self.assertIn("total", data)
def test_create_article(self):
"""Test article creation."""
payload = {
"title": "Test Article",
"content": "Test content",
"is_published": True,
}
response = self.client.post(
"/api/articles/",
data=payload,
content_type="application/json",
**self.auth_headers
)
self.assertEqual(response.status_code, 201)
data = response.json()
self.assertEqual(data["title"], "Test Article")
def test_authentication_required(self):
"""Test that endpoints require authentication."""
response = self.client.get("/api/articles/") # No auth header
self.assertEqual(response.status_code, 401)
Testing with Fixtures
from django.test import TestCase
from myapp.models import Article
class ArticleAPITest(TestCase):
fixtures = ["articles.json"] # Load test data
def test_get_article(self):
"""Test retrieving a specific article."""
response = self.client.get("/api/articles/test-article/", **self.auth_headers)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["slug"], "test-article")
API Documentation
Once installed, automatic documentation is available at:
- Swagger UI:
/api/docs - OpenAPI Schema:
/api/openapi.json - ReDoc:
/api/redoc
Architecture
The package provides:
- APIToken Model - Database-backed authentication tokens
- APIKeyAuth - Bearer token authentication handler
- Auto-Discovery System - Scans apps for
api.pyfiles at startup - Common Schemas - Shared Pydantic schemas (e.g., PaginatedResponse)
- Django System Checks - Validates configuration at startup
- Management Command -
api_discoverfor debugging and validation
Troubleshooting
Discovery and Validation Commands
List all discovered API routers:
python manage.py api_discover --list-routers
Show all registered endpoints:
python manage.py api_discover --list-endpoints
Validate api.py files for common issues:
python manage.py api_discover --validate
Run Django system checks:
python manage.py check
Common Issues
"My endpoints aren't showing up!"
Problem: Created api.py but endpoints don't appear in /api/docs
Solutions:
-
Check INSTALLED_APPS ordering:
INSTALLED_APPS = [ # ... "django_directory_api", # Must come BEFORE your app "myapp", # Your app with api.py # ... ]
-
Verify router export:
# myapp/api.py from ninja import Router router = Router(tags=["My App"]) # ← Must be named 'router' @router.get("/items/") def list_items(request): return {"items": []}
-
Check for syntax errors:
python manage.py api_discover --validate
-
Restart Django server - Changes to
api.pyrequire restart
"ImportError" or "Circular Import"
Problem: Getting import errors when Django starts
Solution: Use local imports in endpoint functions:
# myapp/api.py
from ninja import Router
router = Router(tags=["My App"])
@router.get("/items/")
def list_items(request):
from .models import MyModel # ← Import inside function
return {"items": list(MyModel.objects.values())}
"Router has no tags warning"
Problem: System check warns about missing tags
Solution: Add tags to your router:
router = Router(tags=["My App"]) # ← Helps organize OpenAPI docs
"APIToken table does not exist"
Problem: Database error on startup
Solution: Run migrations:
python manage.py migrate django_directory_api
Debug Output
The package prints discovery information on startup:
[django-directory-api] Auto-discovered and registered 3 API routers
If you see 0 API routers, check:
- INSTALLED_APPS ordering
- Router export names (
routerorrouters) - Syntax errors in api.py files
Development
# Install dependencies
uv sync --extra dev
# Run tests
python tests/manage.py test
# Format code
ruff format .
# Lint
ruff check .
License
MIT License - see LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_directory_api-0.1.1.tar.gz.
File metadata
- Download URL: django_directory_api-0.1.1.tar.gz
- Upload date:
- Size: 17.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d11925a67fe05be729ac81a33d35f1ec50caf61b357abf0f970fc992e033f9cb
|
|
| MD5 |
b1de3c2f0e9383b18c5fd146d9b127aa
|
|
| BLAKE2b-256 |
6b54d677d18edf929c3f52aa3c828812b2eb7759de636f3588f3f838f1133797
|
Provenance
The following attestation bundles were made for django_directory_api-0.1.1.tar.gz:
Publisher:
publish.yml on heysamtexas/django-directory-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_directory_api-0.1.1.tar.gz -
Subject digest:
d11925a67fe05be729ac81a33d35f1ec50caf61b357abf0f970fc992e033f9cb - Sigstore transparency entry: 607700164
- Sigstore integration time:
-
Permalink:
heysamtexas/django-directory-api@fb44337054f81318635764382f8098ce3d912931 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/heysamtexas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fb44337054f81318635764382f8098ce3d912931 -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_directory_api-0.1.1-py3-none-any.whl.
File metadata
- Download URL: django_directory_api-0.1.1-py3-none-any.whl
- Upload date:
- Size: 21.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95d9d590f8d3c01f99c792ee3856cf25c8def19affc5e4b6b4f0e881426f5ba1
|
|
| MD5 |
26f61a38e8dd806251131361019def76
|
|
| BLAKE2b-256 |
1fb8d52f5bdbd7215fb0d74f23b8e3093eb34680722149280e8fe649718415a6
|
Provenance
The following attestation bundles were made for django_directory_api-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on heysamtexas/django-directory-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_directory_api-0.1.1-py3-none-any.whl -
Subject digest:
95d9d590f8d3c01f99c792ee3856cf25c8def19affc5e4b6b4f0e881426f5ba1 - Sigstore transparency entry: 607700166
- Sigstore integration time:
-
Permalink:
heysamtexas/django-directory-api@fb44337054f81318635764382f8098ce3d912931 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/heysamtexas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fb44337054f81318635764382f8098ce3d912931 -
Trigger Event:
release
-
Statement type: