Architecture

Deep dive into the ZineCore2 Django server 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

Learn more →

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

Learn more →

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

TablePurposeKey Fields
catalog_zineZineCore2 recordsid, zine_id, title, creator
agents_agentAgentCore2 recordsid, agent_id, display_name
holdings_holdingHoldingCore2 recordsid, holding_id, zine_id, repo_id
repositories_repositoryRepoCore2 recordsid, repo_id, repository_name
catalog_subjectSubject vocabularycode, label, uri
catalog_genreGenre vocabularycode, 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.

Learn more →

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.

Learn more →

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.

# 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:

Understanding the architecture? Continue to Models for a detailed look at the Django models.
Copyright ©2026 ZineCore2 Contributors,