Reference Implementation

Django REST API server for ZineCore2 metadata

The ZineCore2 reference implementation is a production-ready Django REST API server that implements all four profiles (ZineCore2, AgentCore2, HoldingCore2, RepoCore2) with full CRUD operations, content negotiation, and multiple export formats.

What It Provides

    • RESTful API for all four ZineCore2 profiles
    • PostgreSQL backend with optimized schema design
    • JSON Schema validation on all writes
    • Multiple output formats — JSON, JSON-LD, CSV, DC XML, RDF/Turtle, BibTeX, MARCXML
    • Content negotiation via HTTP Accept headers
    • Controlled vocabularies loaded from specification
    • Authentication & permissions — Public reads, authenticated writes
    • Production-ready — Battle-tested Django REST Framework

Technology Stack

ComponentTechnologyVersion
LanguagePython3.12+
FrameworkDjango6.x
APIDjango REST FrameworkLatest
DatabasePostgreSQL16+
Package ManageruvLatest
DeploymentDocker / systemd + nginx-

Architecture Highlights

1. Dual Identifier Pattern

Internal PKs + External IDs:

  • Integer BigAutoField primary keys (internal, auto-increment)
  • Separate CharField for external identifiers (zine_id, agent_id, repo_id, holding_id)
  • Users interact with external IDs; database uses internal PKs for efficiency

Example:

class Zine(models.Model):
    id = models.BigAutoField(primary_key=True)  # Internal: 12345
    zine_id = models.CharField(max_length=255, unique=True)  # External: "zine_mutate_3_1st"
    title = models.CharField(max_length=500)
    # ... more fields

Why? Separates database performance concerns from API design. External IDs can be human-readable while internal PKs remain optimized integers.

2. ArrayField for Repeatable Elements

Uses PostgreSQL ArrayField for fields that can have multiple values:

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 terms from subjects vocabulary"
    )

Why? Avoids join tables for simple repeatable fields, improving query performance and simplifying API responses.

3. Read/Write Serializer Pattern

Separate serializers for reading and writing:

Read Serializer:

class ZineReadSerializer(serializers.ModelSerializer):
    # Resolves agent_ids to full agent objects
    creator = AgentReadSerializer(many=True, source='creator_agents')

Write Serializer:

class ZineWriteSerializer(serializers.ModelSerializer):
    # Accepts just the IDs
    creator = serializers.ListField(child=serializers.CharField())

Why? Reads return fully resolved nested objects for convenience. Writes accept simple ID references for ease of use.

4. App Structure

backend/
├── core/              # Shared utilities, base models
├── catalog/           # ZineCore2 implementation
│   ├── models.py      # Zine model
│   ├── serializers.py # Read/write serializers
│   └── views.py       # API endpoints
├── agents/            # AgentCore2 implementation
├── holdings/          # HoldingCore2 implementation
├── repositories/      # RepoCore2 implementation
├── geography/         # Countries, regions (ISO 3166)
├── languages/         # Language codes (ISO 639)
└── vocabularies/      # Controlled vocabularies

Each app follows the same pattern:

  • models.py — Django models (one per profile)
  • serializers.py — DRF serializers (read + write)
  • views.py — ViewSets with CRUD operations
  • urls.py — URL routing

5. Database Schema Design

Key decisions:

  1. One table per profile (zines, agents, holdings, repositories)
  2. ForeignKey relationships where appropriate (holdings → zines, holdings → repositories)
  3. Vocabulary tables for controlled terms with referential integrity
  4. ArrayFields instead of many-to-many for simple repeatability
  5. JSON fields for flexible metadata (when needed)

Example schema:

-- Zines table
CREATE TABLE catalog_zine (
    id BIGSERIAL PRIMARY KEY,
    zine_id VARCHAR(255) UNIQUE NOT NULL,
    title VARCHAR(500) NOT NULL,
    creator VARCHAR(255)[] NOT NULL,  -- Array of agent_ids
    subject VARCHAR(100)[] NOT NULL,  -- Array of subject codes
    genre VARCHAR(100)[] NOT NULL,    -- Array of genre codes
    date VARCHAR(100)[] NOT NULL,
    language VARCHAR(10)[] NOT NULL,
    rights VARCHAR(100)[] NOT NULL,
    -- ... more fields
);

6. Content Negotiation

Supports multiple output formats via HTTP Accept headers:

FormatAccept HeaderUse Case
JSON (default)application/jsonAPI consumption, web apps
JSON-LDapplication/ld+jsonSemantic web, RDF integration
CSVtext/csvSpreadsheet export, data analysis
DC XMLapplication/xmlDublin Core XML serialization
RDF/Turtletext/turtleTriple stores, SPARQL
BibTeXapplication/x-bibtexCitation management
MARCXMLapplication/marcxml+xmlLibrary systems integration

Example:

# JSON (default)
curl https://localhost:8000/api/zines/zine_mutate_3_1st/

# JSON-LD
curl -H "Accept: application/ld+json" \
  https://localhost:8000/api/zines/zine_mutate_3_1st/

# CSV
curl -H "Accept: text/csv" \
  https://localhost:8000/api/zines/

7. Authentication & Permissions

Default policy:

  • Read operations — Public, no authentication required
  • Write operations — Require authentication (token-based)

Token authentication:

# Get token (after creating user)
curl -X POST https://localhost:8000/api/auth/token/ \
  -d "username=admin&password=yourpassword"

# Use token
curl -H "Authorization: Token abc123..." \
  -X POST https://localhost:8000/api/zines/ \
  -d '{"title": "My Zine", ...}'

Customizable: Can be configured for institution-specific access control.

API Endpoints

ZineCore2 (Zines)

GET    /api/zines/              # List all zines
POST   /api/zines/              # Create zine (auth required)
GET    /api/zines/{zine_id}/    # Get specific zine
PUT    /api/zines/{zine_id}/    # Update zine (auth required)
PATCH  /api/zines/{zine_id}/    # Partial update (auth required)
DELETE /api/zines/{zine_id}/    # Delete zine (auth required)

AgentCore2 (Creators/Contributors)

GET    /api/agents/             # List all agents
POST   /api/agents/             # Create agent (auth required)
GET    /api/agents/{agent_id}/  # Get specific agent
PUT    /api/agents/{agent_id}/  # Update agent (auth required)
DELETE /api/agents/{agent_id}/  # Delete agent (auth required)

HoldingCore2 (Holdings)

GET    /api/holdings/                # List all holdings
POST   /api/holdings/                # Create holding (auth required)
GET    /api/holdings/{holding_id}/   # Get specific holding
PUT    /api/holdings/{holding_id}/   # Update holding (auth required)
DELETE /api/holdings/{holding_id}/   # Delete holding (auth required)

RepoCore2 (Repositories)

GET    /api/repositories/         # List all repositories
POST   /api/repositories/         # Create repository (auth required)
GET    /api/repositories/{repo_id}/  # Get specific repository
PUT    /api/repositories/{repo_id}/  # Update repository (auth required)
DELETE /api/repositories/{repo_id}/  # Delete repository (auth required)

Vocabularies

GET    /api/vocabularies/           # List all vocabularies
GET    /api/vocabularies/{vocab_id}/ # Get specific vocabulary

Quick Start

Prerequisites

  • Python 3.12+ with uv package manager
  • PostgreSQL 16+ database
  • Git

Installation (Development)

# Clone repository
git clone https://github.com/ZineCore2/server.git
cd server/

# Run interactive onboarding
./onboarding.sh

# Start development server
./start.sh

# Server running at http://localhost:8000

The onboarding.sh script will:

  1. Check prerequisites
  2. Create Python virtual environment with uv
  3. Install dependencies
  4. Set up PostgreSQL database
  5. Run migrations
  6. Load vocabularies from spec
  7. Create superuser account

Full Installation Guide →

Data Management

Loading Vocabularies

Vocabularies are loaded from the spec/ submodule:

# Load all vocabularies
.venv/bin/python backend/manage.py load_vocabularies

# Load specific vocabulary
.venv/bin/python backend/manage.py load_vocabularies --vocab subjects

Source: /Users/andrew/Develop/ZineCore2/spec/vocabularies/canonical/*.json

Database Migrations

# Create migrations after model changes
.venv/bin/python backend/manage.py makemigrations

# Apply migrations
.venv/bin/python backend/manage.py migrate

# Show migration status
.venv/bin/python backend/manage.py showmigrations

Management Commands

CommandPurpose
load_vocabulariesLoad controlled vocabularies from spec
load_geonamesLoad geographic data (countries, regions)
load_languagesLoad ISO 639 language codes
load_countriesLoad ISO 3166 country codes

Development Workflow

# Activate virtual environment
source .venv/bin/activate

# Run development server
cd backend/
python manage.py runserver

# Run tests
pytest

# Check code quality
ruff check .
black --check .

# Apply formatting
black .

Deployment

Option 1: Docker

# Build image
docker build -t zinecore2-server .

# Run container
docker run -p 8000:8000 \
  -e DATABASE_URL=postgresql://... \
  zinecore2-server

Option 2: systemd + nginx

# Configure systemd service
sudo cp zinecore2.service /etc/systemd/system/
sudo systemctl enable zinecore2
sudo systemctl start zinecore2

# Configure nginx reverse proxy
sudo cp nginx-zinecore2.conf /etc/nginx/sites-available/
sudo ln -s /etc/nginx/sites-available/nginx-zinecore2.conf \
  /etc/nginx/sites-enabled/
sudo systemctl reload nginx

Full Deployment Guide →

Configuration

Environment Variables

# Development
DJANGO_SETTINGS_MODULE=zinecore.settings.development

# Production
DJANGO_SETTINGS_MODULE=zinecore.settings.production
DATABASE_URL=postgresql://user:pass@host/dbname
SECRET_KEY=your-secret-key-here
ALLOWED_HOSTS=zinecore.example.com
CORS_ALLOWED_ORIGINS=https://yourfrontend.com

Settings Modules

  • zinecore.settings.development — Development with DEBUG=True
  • zinecore.settings.production — Production with DEBUG=False
  • zinecore.settings.testing — Test suite configuration

Next Steps

Ready to get started? Follow the Installation Guide to set up the reference implementation.
Copyright ©2026 ZineCore2 Contributors,