Collection+JSON — Hypermedia Format for Collection-Oriented APIs
In this tutorial, you will learn about Collection+JSON. We cover key concepts, practical examples, and best practices to help you master this topic.
Collection+JSON is a hypermedia JSON format designed for collection-oriented APIs, providing standardized structures for listing, searching, creating, updating, and deleting items with hypermedia controls.
What You'll Learn
- Collection+JSON document structure
- Query templates for search/filter
- Write templates for create/update
- Pagination with collection links
- Error representation
- Implementing Collection+JSON in Node.js
Why It Matters
Many REST APIs are fundamentally about collections — devices, threats, users. Collection+JSON provides a standardized way to represent these collections with hypermedia controls, making CRUD operations discoverable. DodaTech's Durga Antivirus Pro uses Collection+JSON for its threat list API, letting clients discover filtering, sorting, and creation options dynamically.
Real-World Use
A client fetches the threat collection and discovers a query template with fields for severity, deviceId, and dateRange. The dashboard renders a filter form from the template. When the server adds a new filter field, the dashboard updates automatically.
flowchart TB
A["GET /threats"] --> B["Collection+JSON"]
B --> C["collection.links: [self, next, prev]"]
B --> D["collection.items: [threat, threat, ...]"]
B --> E["collection.queries: [search, filter]"]
B --> F["collection.template: { create data }"]
E --> G["search: { fields: [severity, deviceId] }"]
F --> H["create: { fields: [name, severity, deviceId] }"]
style B fill:#dbeafe,stroke:#2563eb
Code Examples
Example 1: Collection+JSON Document
{
"collection": {
"version": "1.0",
"href": "https://api.dodatech.com/threats",
"links": [
{ "rel": "self", "href": "/threats?page=1" },
{ "rel": "next", "href": "/threats?page=2" },
{ "rel": "last", "href": "/threats?page=10" },
{ "rel": "devices", "href": "/devices" }
],
"items": [
{
"href": "/threats/101",
"data": [
{ "name": "threatId", "value": "101", "prompt": "Threat ID" },
{ "name": "name", "value": "Ransomware-X", "prompt": "Threat Name" },
{ "name": "severity", "value": "critical", "prompt": "Severity" },
{ "name": "deviceId", "value": "1", "prompt": "Device ID" },
{ "name": "detectedAt", "value": "2026-06-28T10:00:00Z", "prompt": "Detected At" }
],
"links": [
{ "rel": "self", "href": "/threats/101" },
{ "rel": "device", "href": "/devices/1" },
{ "rel": "analyze", "href": "/threats/101/analyze" }
]
}
],
"queries": [
{
"rel": "search",
"href": "/threats",
"prompt": "Search Threats",
"data": [
{ "name": "severity", "value": "", "prompt": "Filter by severity" },
{ "name": "deviceId", "value": "", "prompt": "Filter by device" },
{ "name": "from", "value": "", "prompt": "Start date" },
{ "name": "to", "value": "", "prompt": "End date" }
]
}
],
"template": {
"data": [
{ "name": "name", "value": "", "prompt": "Threat name" },
{ "name": "severity", "value": "medium", "prompt": "Severity" },
{ "name": "deviceId", "value": "", "prompt": "Device ID" }
]
},
"error": null
}
}
Example 2: Collection+JSON Server
const express = require('express');
const app = express();
app.use(express.json());
function cjResponse(href, options = {}) {
const collection = {
version: '1.0',
href,
links: options.links || [],
items: options.items || [],
queries: options.queries || [],
template: options.template || null,
error: options.error || null,
};
if (!options.error) delete collection.error;
if (!options.template) delete collection.template;
return { collection };
}
function cjItem(href, data, links) {
return {
href,
data: data.map(d => ({
name: d.name,
value: d.value,
prompt: d.prompt || d.name,
})),
links: links || [],
};
}
// Collection endpoint
app.get('/threats', (req, res) => {
const page = parseInt(req.query.page) || 1;
const { severity, deviceId } = req.query;
const threats = queryThreats({ page, severity, deviceId });
const totalPages = getTotalPages();
const paginationLinks = [
{ rel: 'self', href: `/threats?page=${page}` },
];
if (page < totalPages) {
paginationLinks.push({ rel: 'next', href: `/threats?page=${page + 1}` });
}
if (page > 1) {
paginationLinks.push({ rel: 'prev', href: `/threats?page=${page - 1}` });
}
paginationLinks.push({ rel: 'last', href: `/threats?page=${totalPages}` });
const response = cjResponse('/threats', {
links: paginationLinks,
items: threats.map(t => cjItem(
`/threats/${t.id}`,
[
{ name: 'threatId', value: t.id, prompt: 'ID' },
{ name: 'name', value: t.name, prompt: 'Name' },
{ name: 'severity', value: t.severity, prompt: 'Severity' },
],
[
{ rel: 'self', href: `/threats/${t.id}` },
{ rel: 'device', href: `/devices/${t.deviceId}` },
],
)),
queries: [
{
rel: 'search',
href: '/threats',
prompt: 'Search',
data: [
{ name: 'severity', value: severity || '', prompt: 'Severity' },
{ name: 'deviceId', value: deviceId || '', prompt: 'Device' },
],
},
],
template: {
data: [
{ name: 'name', value: '', prompt: 'Threat name (required)' },
{ name: 'severity', value: 'medium', prompt: 'Severity' },
{ name: 'deviceId', value: '', prompt: 'Device ID' },
],
},
});
res.json(response);
});
// Create via template
app.post('/threats', (req, res) => {
const threat = createThreat(req.body);
res.status(201).json(cjResponse('/threats', {
items: [cjItem(`/threats/${threat.id}`, [
{ name: 'threatId', value: threat.id },
{ name: 'name', value: threat.name },
])],
}));
});
Example 3: Collection+JSON Client
class CollectionJsonClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(href) {
const res = await fetch(`${this.baseUrl}${href}`);
return res.json();
}
async query(collection, queryRel, params) {
const queryDef = collection.collection.queries
.find(q => q.rel === queryRel);
if (!queryDef) throw new Error(`Query '${queryRel}' not found`);
const url = new URL(queryDef.href, this.baseUrl);
for (const [key, value] of Object.entries(params)) {
if (value) url.searchParams.set(key, value);
}
return this.get(url.pathname + url.search);
}
async create(collection, data) {
const template = collection.collection.template;
if (!template) throw new Error('No write template');
// Validate template fields
const body = {};
for (const field of template.data) {
body[field.name] = data[field.name] || field.value;
}
const res = await fetch(`${this.baseUrl}/threats`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return res.json();
}
async paginate(collection, direction = 'next') {
const link = collection.collection.links
.find(l => l.rel === direction);
if (!link) return null; // No more pages
return this.get(link.href);
}
async searchThreats() {
const collection = await this.get('/threats');
// Discover search from queries
const results = await this.query(collection, 'search', {
severity: 'critical',
});
// Paginate through results
let page = results;
while (page) {
for (const item of page.collection.items) {
console.log('Threat:', item.data.find(d => d.name === 'name').value);
}
page = await this.paginate(page, 'next');
}
}
}
Common Mistakes
- Not including self link — the collection must have a self link pointing to its own URL. This is required for cache validation and identity.
- Using flat data arrays without prompts — prompts tell clients what each field means. Without prompts, the data array is meaningless.
- Not providing a write template — a POST endpoint without a template forces clients to guess the required fields. Include the template with all fields.
- Confusing query templates with write templates — queries filter the collection (GET), templates create new items (POST). They serve different purposes.
- Omitting error field on errors — when an error occurs, include the error object with code and message instead of returning a non-Collection+JSON response.
Practice Questions
- What is the structure of a Collection+JSON document?
- How do query templates enable search discoverability?
- How does the write template guide item creation?
- How is pagination represented in Collection+JSON?
- What is the purpose of the
promptfield in data arrays?
Challenge: Design a Collection+JSON API for a device inventory system with: paginated device list, search query with 5 filter fields, write template for device creation, and error handling for validation failures.
Mini Project
Build a Collection+JSON API for a threat management system with: paginated threat collection, query templates for filtering by severity and date, write template for creating threats, item-level links to related resources, and a JavaScript client that discovers and uses all templates.
FAQ
What's Next
Learn about hypermedia link formats
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro