REST API Development

HATEOAS & Hypermedia APIs

20 min Lesson 16 of 35

Introduction to HATEOAS

HATEOAS (Hypermedia As The Engine Of Application State) is a constraint of the REST application architecture that distinguishes it from other network architectures. The principle is that a client interacts with a network application entirely through hypermedia provided dynamically by application servers. A REST client needs no prior knowledge about how to interact with an application or server beyond a generic understanding of hypermedia.

Core Concept: HATEOAS means that your API responses include links to related resources and possible actions, allowing clients to discover functionality dynamically rather than hardcoding URLs.

Why HATEOAS Matters

HATEOAS provides several significant advantages for API design:

  • Discoverability: Clients can navigate the API by following links without needing documentation for every endpoint
  • Flexibility: Server URLs can change without breaking clients, as long as link relations remain consistent
  • Self-Documentation: The API response tells clients what actions are available
  • Reduced Coupling: Clients depend on link relations rather than hardcoded URLs
  • State Management: Links can indicate available state transitions

Understanding Link Relations

Link relations define the relationship between the current resource and the linked resource. Common link relations include:

<?php // Standard IANA Link Relations $linkRelations = [ \047self\047 => \047Link to the resource itself\047, \047next\047 => \047Link to the next resource in a collection\047, \047prev\047 => \047Link to the previous resource\047, \047first\047 => \047Link to the first resource in a collection\047, \047last\047 => \047Link to the last resource\047, \047edit\047 => \047Link to edit the resource\047, \047delete\047 => \047Link to delete the resource\047, \047collection\047 => \047Link to the parent collection\047, \047related\047 => \047Link to a related resource\047, ]; // Custom application-specific relations $customRelations = [ \047author\047 => \047Link to the author of the resource\047, \047comments\047 => \047Link to comments on the resource\047, \047publish\047 => \047Action to publish the resource\047, \047approve\047 => \047Action to approve the resource\047, ];

Basic HATEOAS Implementation

Let\047s start with a simple implementation that adds links to a resource response:

<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource { public function toArray($request) { return [ \047id\047 => $this->id, \047title\047 => $this->title, \047content\047 => $this->content, \047author\047 => $this->author->name, \047created_at\047 => $this->created_at->toIso8601String(), \047links\047 => [ \047self\047 => [ \047href\047 => route(\047api.posts.show\047, $this->id), \047method\047 => \047GET\047, ], \047update\047 => [ \047href\047 => route(\047api.posts.update\047, $this->id), \047method\047 => \047PUT\047, ], \047delete\047 => [ \047href\047 => route(\047api.posts.destroy\047, $this->id), \047method\047 => \047DELETE\047, ], \047author\047 => [ \047href\047 => route(\047api.users.show\047, $this->author_id), \047method\047 => \047GET\047, ], \047comments\047 => [ \047href\047 => route(\047api.posts.comments.index\047, $this->id), \047method\047 => \047GET\047, ], ], ]; } }

This produces a response like:

{ "id": 42, "title": "Understanding HATEOAS", "content": "HATEOAS is a powerful concept...", "author": "John Doe", "created_at": "2026-02-14T10:30:00Z", "links": { "self": { "href": "https://api.example.com/posts/42", "method": "GET" }, "update": { "href": "https://api.example.com/posts/42", "method": "PUT" }, "delete": { "href": "https://api.example.com/posts/42", "method": "DELETE" }, "author": { "href": "https://api.example.com/users/10", "method": "GET" }, "comments": { "href": "https://api.example.com/posts/42/comments", "method": "GET" } } }

Understanding HAL (Hypertext Application Language)

HAL is a simple format that gives a consistent and easy way to hyperlink between resources in your API. It was created to standardize HATEOAS implementation and includes two main elements: resources and links.

HAL Structure: HAL uses _links for hypermedia controls and _embedded for nested resources, making the structure predictable and easy to parse.

Implementing HAL Format

Here\047s how to implement HAL format in Laravel:

<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class HalPostResource extends JsonResource { public function toArray($request) { return [ \047id\047 => $this->id, \047title\047 => $this->title, \047content\047 => $this->content, \047status\047 => $this->status, \047published_at\047 => $this->published_at, // HAL _links section \047_links\047 => [ \047self\047 => [ \047href\047 => route(\047api.posts.show\047, $this->id), ], \047author\047 => [ \047href\047 => route(\047api.users.show\047, $this->author_id), \047title\047 => $this->author->name, ], \047comments\047 => [ \047href\047 => route(\047api.posts.comments.index\047, $this->id), \047count\047 => $this->comments_count, ], ], // HAL _embedded section for related resources \047_embedded\047 => [ \047author\047 => [ \047id\047 => $this->author->id, \047name\047 => $this->author->name, \047email\047 => $this->author->email, \047_links\047 => [ \047self\047 => [ \047href\047 => route(\047api.users.show\047, $this->author_id), ], ], ], ], ]; } }

State-Based Link Generation

One of the most powerful aspects of HATEOAS is showing only available actions based on the resource\047s current state:

<?php class StatefulPostResource extends JsonResource { public function toArray($request) { $links = [ \047self\047 => route(\047api.posts.show\047, $this->id), ]; // Add state-specific links if ($this->status === \047draft\047) { $links[\047publish\047] = [ \047href\047 => route(\047api.posts.publish\047, $this->id), \047method\047 => \047POST\047, ]; $links[\047edit\047] = [ \047href\047 => route(\047api.posts.update\047, $this->id), \047method\047 => \047PUT\047, ]; } if ($this->status === \047published\047) { $links[\047unpublish\047] = [ \047href\047 => route(\047api.posts.unpublish\047, $this->id), \047method\047 => \047POST\047, ]; } // Only allow deletion if post has no comments if ($this->comments_count === 0) { $links[\047delete\047] = [ \047href\047 => route(\047api.posts.destroy\047, $this->id), \047method\047 => \047DELETE\047, ]; } // Only allow editing if user is the author if ($this->author_id === $request->user()->id) { $links[\047edit\047] = [ \047href\047 => route(\047api.posts.update\047, $this->id), \047method\047 => \047PUT\047, ]; } return [ \047id\047 => $this->id, \047title\047 => $this->title, \047status\047 => $this->status, \047_links\047 => $links, ]; } }

Creating a Reusable Link Builder

To avoid repetition, create a dedicated service for building links:

<?php namespace App\Services; class LinkBuilder { protected $links = []; public function add(string $rel, string $href, string $method = \047GET\047, array $extra = []) { $this->links[$rel] = array_merge([ \047href\047 => $href, \047method\047 => $method, ], $extra); return $this; } public function addIf(bool $condition, string $rel, string $href, string $method = \047GET\047) { if ($condition) { $this->add($rel, $href, $method); } return $this; } public function collection(string $rel, string $href) { return $this->add($rel, $href, \047GET\047, [\047type\047 => \047collection\047]); } public function toArray(): array { return $this->links; } } // Usage in Resource public function toArray($request) { $linkBuilder = new LinkBuilder(); $linkBuilder->add(\047self\047, route(\047api.posts.show\047, $this->id)) ->addIf($this->canEdit(), \047edit\047, route(\047api.posts.update\047, $this->id), \047PUT\047) ->addIf($this->canDelete(), \047delete\047, route(\047api.posts.destroy\047, $this->id), \047DELETE\047) ->collection(\047comments\047, route(\047api.posts.comments.index\047, $this->id)); return [ \047id\047 => $this->id, \047title\047 => $this->title, \047_links\047 => $linkBuilder->toArray(), ]; }

Pagination with HATEOAS

HATEOAS is especially valuable for paginated collections:

<?php class PostCollection extends ResourceCollection { public function toArray($request) { return [ \047data\047 => $this->collection, \047_links\047 => [ \047self\047 => [ \047href\047 => $request->url(), ], \047first\047 => [ \047href\047 => $this->url(1), ], \047last\047 => [ \047href\047 => $this->url($this->lastPage()), ], \047prev\047 => $this->previousPageUrl() ? [ \047href\047 => $this->previousPageUrl(), ] : null, \047next\047 => $this->nextPageUrl() ? [ \047href\047 => $this->nextPageUrl(), ] : null, ], \047meta\047 => [ \047current_page\047 => $this->currentPage(), \047last_page\047 => $this->lastPage(), \047per_page\047 => $this->perPage(), \047total\047 => $this->total(), ], ]; } }

Common Mistake: Don\047t include links that the current user doesn\047t have permission to access. Always check authorization before adding action links to avoid confusing or misleading clients.

Advanced: Form Templates

For complex operations, you can include form templates that describe required fields:

<?php public function toArray($request) { return [ \047id\047 => $this->id, \047title\047 => $this->title, \047_links\047 => [ \047update\047 => [ \047href\047 => route(\047api.posts.update\047, $this->id), \047method\047 => \047PUT\047, \047template\047 => [ \047title\047 => [ \047type\047 => \047string\047, \047required\047 => true, \047maxLength\047 => 255, ], \047content\047 => [ \047type\047 => \047text\047, \047required\047 => true, ], \047status\047 => [ \047type\047 => \047enum\047, \047required\047 => true, \047options\047 => [\047draft\047, \047published\047, \047archived\047], ], \047category_id\047 => [ \047type\047 => \047integer\047, \047required\047 => false, ], ], ], ], ]; }

Exercise: Build a HATEOAS API

Create a complete HATEOAS implementation for a product API:

  1. Create a ProductResource that includes links for view, edit, delete, reviews, and related products
  2. Implement state-based links (e.g., "add to cart" only if in stock, "reorder" only if out of stock)
  3. Create a ProductCollection with pagination links
  4. Build a LinkBuilder service to keep your resources DRY
  5. Test with a client application that follows links instead of hardcoded URLs

Best Practices for HATEOAS

  • Use Standard Relations: Prefer IANA-registered link relations when possible
  • Be Consistent: Use the same link structure across all resources
  • Include Methods: Always specify the HTTP method for each link
  • Document Custom Relations: Clearly document any custom link relations you create
  • Versioning: Link relations should be stable across API versions
  • Conditional Links: Only include links the client can actually use
  • Performance: Consider caching strategies for expensive link generation
  • Documentation: Even with HATEOAS, provide clear documentation of available relations

Real-World Impact: Companies like GitHub and PayPal use HATEOAS extensively in their APIs, allowing them to evolve their services without breaking client applications. This flexibility becomes invaluable as your API grows and changes over time.