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:
- Create a ProductResource that includes links for view, edit, delete, reviews, and related products
- Implement state-based links (e.g., "add to cart" only if in stock, "reorder" only if out of stock)
- Create a ProductCollection with pagination links
- Build a LinkBuilder service to keep your resources DRY
- 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.