Architecture
The ZineCore2 reference implementation uses Django 6.x with Django REST Framework to provide a production-ready API for managing zine metadata. This section explains the architectural decisions and patterns used throughout the codebase.
High-Level Overview
┌─────────────────────────────────────────────────────────────┐
│ HTTP Requests │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Django REST Framework │
│ • ViewSets (CRUD operations) │
│ • Serializers (validation, transformation) │
│ • Authentication & Permissions │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Django Models │
│ • Zine, Agent, Holding, Repository │
│ • Vocabularies (Subject, Genre, etc.) │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL Database │
│ • Tables with BigAutoField PKs │
│ • ArrayField for repeatable elements │
│ • Foreign keys for relationships │
└─────────────────────────────────────────────────────────────┘
Key Architectural Patterns
1. Dual Identifier Pattern
Problem: Database PKs should be integers for performance, but external APIs should use human-readable IDs.
Solution: Separate internal and external identifiers.
class Zine(models.Model):
# Internal PK (never exposed via API)
id = models.BigAutoField(primary_key=True)
# External ID (used in API URLs)
zine_id = models.CharField(max_length=255, unique=True)
# Metadata fields
title = models.CharField(max_length=500)
# ...
Benefits:
- Database uses efficient integer PKs
- API uses readable IDs like zine_mutate_3_1st
- IDs can be changed without breaking database integrity
- Supports natural keys for fixtures
2. Read/Write Serializer Split
Problem: Reading records should return full nested objects, but writing should accept simple ID references.
Solution: Separate serializers for reading and writing.
# Read serializer - returns full nested objects
class ZineReadSerializer(serializers.ModelSerializer):
creator = AgentReadSerializer(many=True, source='creator_agents')
# Write serializer - accepts just IDs
class ZineWriteSerializer(serializers.ModelSerializer):
creator = serializers.ListField(child=serializers.CharField())
Benefits:
- Read responses are fully resolved (convenient for clients)
- Write requests are simple (just provide IDs)
- Validation happens on write
- Different logic for input vs. output
3. App-Per-Profile Organization
Structure:
backend/
├── catalog/ # ZineCore2
├── agents/ # AgentCore2
├── holdings/ # HoldingCore2
├── repositories/ # RepoCore2
├── vocabularies/ # Shared vocabularies
├── geography/ # Geographic data
└── core/ # Shared utilities
Benefits:
- Clear separation of concerns
- Each app is self-contained
- Easy to understand and navigate
- Follows Django best practices
4. PostgreSQL-Specific Features
Uses PostgreSQL ArrayField for repeatable elements:
class Zine(models.Model):
creator = models.ArrayField(
models.CharField(max_length=255),
help_text="Agent IDs from AgentCore2"
)
subject = models.ArrayField(
models.CharField(max_length=100),
help_text="Subject codes from subjects vocabulary"
)
Benefits:
- Avoids join tables for simple lists
- Better query performance
- Simpler API responses
- Native database support for array operations
Trade-off: Requires PostgreSQL (won't work with SQLite/MySQL)
5. Content Negotiation
Supports multiple output formats via HTTP Accept headers:
# In views.py
from rest_framework.renderers import JSONRenderer
from .renderers import (
JSONLDRenderer,
CSVRenderer,
DCXMLRenderer,
TurtleRenderer,
BibTeXRenderer,
MARCXMLRenderer
)
class ZineViewSet(viewsets.ModelViewSet):
renderer_classes = [
JSONRenderer,
JSONLDRenderer,
CSVRenderer,
# ... more renderers
]
Benefits:
- Single endpoint serves multiple formats
- Standards-compliant (Dublin Core XML, JSON-LD, etc.)
- Easy integration with different systems
Data Flow
Creating a Record (POST)
1. Client POSTs JSON to /api/zines/
↓
2. DRF routes to ZineViewSet.create()
↓
3. ZineWriteSerializer validates data
↓
4. Serializer.save() creates Zine model instance
↓
5. Django ORM inserts row into database
↓
6. ZineReadSerializer formats response
↓
7. JSONRenderer converts to JSON
↓
8. HTTP 201 Created response with full object
Reading a Record (GET)
1. Client GETs /api/zines/zine_mutate_3_1st/
↓
2. DRF routes to ZineViewSet.retrieve()
↓
3. Django ORM queries by zine_id (not id)
↓
4. Related objects loaded (creator agents, etc.)
↓
5. ZineReadSerializer formats response
↓
6. Renderer converts to requested format
↓
7. HTTP 200 OK response
Database Design
Tables
| Table | Purpose | Key Fields |
|---|---|---|
| catalog_zine | ZineCore2 records | id, zine_id, title, creator |
| agents_agent | AgentCore2 records | id, agent_id, display_name |
| holdings_holding | HoldingCore2 records | id, holding_id, zine_id, repo_id |
| repositories_repository | RepoCore2 records | id, repo_id, repository_name |
| catalog_subject | Subject vocabulary | code, label, uri |
| catalog_genre | Genre vocabulary | code, label, uri |
| ... | More vocabularies | ... |
Relationships
-- Holdings reference zines and repositories
ALTER TABLE holdings_holding
ADD FOREIGN KEY (zine_id) REFERENCES catalog_zine(id);
ALTER TABLE holdings_holding
ADD FOREIGN KEY (repository_id) REFERENCES repositories_repository(id);
Note: Foreign keys use internal integer PKs, not external IDs.
Model Hierarchy
All models inherit from base classes:
# core/models.py
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class BaseVocabulary(TimestampedModel):
code = models.CharField(max_length=100, unique=True)
label = models.CharField(max_length=255)
definition = models.TextField(blank=True)
uri = models.URLField()
class Meta:
abstract = True
All profile models inherit TimestampedModel. All vocabulary models inherit BaseVocabulary.
ViewSet Patterns
Profile ViewSets
class ZineViewSet(viewsets.ModelViewSet):
queryset = Zine.objects.all()
lookup_field = 'zine_id' # Use external ID for lookups
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return ZineWriteSerializer
return ZineReadSerializer
Vocabulary ViewSets
class SubjectViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Subject.objects.all()
serializer_class = SubjectSerializer
lookup_field = 'code'
pagination_class = None # No pagination for vocabularies
Authentication & Authorization
Permission Classes
# Default: public reads, authenticated writes
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
Token Authentication
# Obtain token
POST /api/auth/token/
{"username": "admin", "password": "pass"}
# Use token
GET /api/zines/
Authorization: Token abc123...
Performance Optimizations
1. Database Indexing
class Zine(models.Model):
zine_id = models.CharField(max_length=255, unique=True, db_index=True)
# ...
External IDs are indexed for fast lookups.
2. Select Related
# In ViewSet
def get_queryset(self):
return Zine.objects.select_related('repository').prefetch_related('creator_agents')
Reduces N+1 query problems.
3. Pagination
All list endpoints paginated (25 items per page by default).
Next Steps
Explore each architectural component in detail:
- Models — Django models for each profile
- Serializers — Read/write serializer pattern
- External Identifiers — Dual identifier pattern explained
- Database Schema — Table structure and relationships