Skip to content

HATEOAS JSON HAL — Hypermedia Application Language for REST APIs

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about HATEOAS JSON HAL. We cover key concepts, practical examples, and best practices to help you master this topic.

JSON HAL (Hypertext Application Language) is a standard format for representing REST resources with hypermedia links, using _links for navigation and _embedded for related resources, making APIs self-describing and discoverable.

What You'll Learn

  • HAL resource structure with _links and _embedded
  • Curies for link relation documentation
  • HAL browser for API exploration
  • Implementing HAL in Node.js and Java
  • Embedding related resources
  • Link templating with RFC 6570

Why It Matters

HAL provides a consistent, well-documented format for hypermedia APIs. Clients can navigate the API by following links instead of hardcoding URLs. DodaTech's Durga Antivirus Pro uses HAL for its device management API, allowing the dashboard to discover related resources (threats, scans, settings) from each device resource.

Real-World Use

A client fetches a device resource and receives _links to its threats (/devices/1/threats), scan history (/devices/1/scans), and configuration (/devices/1/config). The client renders navigation options without knowing the URL structure in advance.

flowchart TB
    A["GET /devices/1"] --> B["Device Resource (HAL)"]
    B --> C["_links.self: /devices/1"]
    B --> D["_links.threats: /devices/1/threats"]
    B --> E["_links.scans: /devices/1/scans"]
    B --> F["_links.config: /devices/1/config"]
    B --> G["_embedded: { threats: [...] }"]
    C --> H["Client navigates via links"]
    D --> H
    E --> H
    style A fill:#dbeafe,stroke:#2563eb

Code Examples

Example 1: HAL Resource Structure

{
  "_links": {
    "self": { "href": "/devices/1" },
    "threats": { "href": "/devices/1/threats" },
    "scans": { "href": "/devices/1/scans" },
    "config": { "href": "/devices/1/config" },
    "curies": [
      {
        "name": "dt",
        "href": "https://docs.dodatech.com/rels/{rel}",
        "templated": true
      }
    ],
    "dt:quarantine": { "href": "/devices/1/quarantine" },
    "dt:analyze": { "href": "/devices/1/analyze" }
  },
  "_embedded": {
    "threats": [
      {
        "_links": {
          "self": { "href": "/threats/101" }
        },
        "threatId": "101",
        "name": "Ransomware-X"
      }
    ]
  },
  "deviceId": "1",
  "name": "Office-PC",
  "os": "Windows 11",
  "status": "active"
}

Example 2: HAL Implementation in Node.js

const express = require('express');
const app = express();

// HAL helper
function hal(resource, links, embedded = {}) {
  const result = { ...resource, _links: {} };
  
  for (const [rel, href] of Object.entries(links)) {
    if (typeof href === 'object') {
      result._links[rel] = href;
    } else {
      result._links[rel] = { href };
    }
  }
  
  if (Object.keys(embedded).length > 0) {
    result._embedded = embedded;
  }
  
  return result;
}

// Device endpoint with HAL
app.get('/devices/:id', (req, res) => {
  const device = getDevice(req.params.id);
  const threats = getThreatsForDevice(req.params.id);
  
  const response = hal(
    { ...device },
    {
      self: `/devices/${device.id}`,
      threats: `/devices/${device.id}/threats`,
      scans: `/devices/${device.id}/scans`,
      config: `/devices/${device.id}/config`,
      'dt:quarantine': `/devices/${device.id}/quarantine`,
      curies: [{
        name: 'dt',
        href: 'https://docs.dodatech.com/rels/{rel}',
        templated: true,
      }],
    },
    {
      threats: threats.map(t => hal(
        { ...t },
        { self: `/threats/${t.id}` }
      )),
    }
  );
  
  res.json(response);
});

// Collection with HAL
app.get('/devices', (req, res) => {
  const devices = getAllDevices();
  
  const response = hal(
    { total: devices.length },
    {
      self: '/devices',
      next: devices.length > 20 ? '/devices?page=2' : null,
    },
    {
      items: devices.map(d => hal(
        { ...d },
        { self: `/devices/${d.id}` }
      )),
    }
  );
  
  res.json(response);
});

Example 3: HAL Client Navigation

class HalClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.cache = new Map();
  }
  
  async fetch(url) {
    if (this.cache.has(url)) {
      return this.cache.get(url);
    }
    
    const response = await fetch(url);
    const resource = await response.json();
    this.cache.set(url, resource);
    return resource;
  }
  
  async followLink(resource, rel) {
    const link = resource._links[rel];
    if (!link) {
      throw new Error(`Link '${rel}' not found`);
    }
    return this.fetch(link.href);
  }
  
  async navigate() {
    // Start at root
    const root = await this.fetch('/api');
    console.log('Available actions:', Object.keys(root._links));
    
    // Follow to devices
    const devices = await this.followLink(root, 'devices');
    console.log('Devices:', devices._embedded.items.length);
    
    // Follow to first device's threats
    const firstDevice = devices._embedded.items[0];
    const threats = await this.followLink(firstDevice, 'threats');
    console.log('Threats:', threats);
    
    // Follow to quarantine action
    await this.followLink(firstDevice, 'dt:quarantine');
    console.log('Device quarantined');
  }
}

// Usage
const client = new HalClient('https://api.dodatech.com');
await client.navigate();

Common Mistakes

  1. Not including a self link — every resource must have a self link that returns the current resource. Clients use this for Caching and identity.
  2. Hardcoding URLs in clients — the point of HATEOAS is that clients discover URLs via links. Never hardcode URLs when you can follow links.
  3. Forgetting curies for custom relations — custom link relations without documentation are meaningless. Use curies to link to documentation.
  4. Embedding too much data — _embedded is for related resources the client likely needs immediately. Don't embed entire collections; use paginated links instead.
  5. Using _links as an afterthought — links should be a first-class part of the resource design, not added as an afterthought.

Practice Questions

  1. What is the purpose of the _links property in HAL?
  2. How do curies help document custom link relations?
  3. When should you use _embedded versus separate links?
  4. How does a HAL client navigate an API without documentation?
  5. What is the difference between a link and an embedded resource?

Challenge: Design a HAL API for a threat management system where each device resource links to its threats, scans, and configuration. Include curies for documentation, embedded recent threats, and a collection endpoint with pagination links.

Mini Project

Build a HAL-based REST API for a device management system with: HAL-formatted resources with self links, curies for custom relations, embedded related resources, collection endpoints with pagination links, and a JavaScript HAL client that navigates the API programmatically.

FAQ

What is the difference between HAL and other hypermedia formats?

HAL focuses on simplicity with _links and _embedded. Siren adds action/field metadata. JSON-LD adds semantic web context. HAL is the most widely adopted.

How do I handle pagination in HAL?

Use next, prev, first, last links in the collection resource. The link href contains the page cursor. Clients follow next to paginate.

Should I always embed related resources?

Embed resources that are small and likely needed by most clients. For large or optional collections, provide links instead.

How does HAL handle errors?

HAL doesn't define error format. Use RFC 7807 Problem Details for errors, which can include _links for remediation actions.

Can I use HAL with non-JSON formats?

HAL is primarily JSON-focused. For XML, use Atom or XHTML. For YAML, HAL isn't commonly used; consider Collection+JSON.

What's Next

Learn about HATEOAS link formats

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro