fsc/hateoas-bundle
Symfony bundle that adds HATEOAS hypermedia links to your API responses. Define relations and links with annotations, integrate with JMS Serializer and Symfony routing, and expose discoverable HAL/JSON-LD resources so clients can navigate your API at runtime.
First, the reality check: fsc/hateoas-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, and no config/hateoas.php. Its composer constraints target Symfony 2.x/3.x and an era of PHP that predates Laravel 8. On a modern Laravel app composer require fsc/hateoas-bundle will either fail dependency resolution or drag in a symfony/framework-bundle you cannot boot. Treat this package as a reference implementation and pattern source, not a drop-in dependency.
If you are in a legacy Symfony service (the only place it actually runs):
composer require fsc/hateoas-bundle alongside jms/serializer-bundle — the bundle hooks the JMS serialization pipeline, not Symfony's normalizer.new FSC\HateoasBundle\FSCHateoasBundle() in AppKernel.fsc_hateoas block to config.yml (metadata cache, links key) and ensure your API routes are named.Relation and serialize the entity through JMS; the response gains a _links (or configured) key.Where to look first: the README is effectively the manual — there is no docs site. Then Annotation/ (Relation, Embedded, Exclusion), Serializer/Handler/ (where links are injected into the JSON), and Resources/config/ for the service ids you would decorate.
First use case: a GET /users/{id} endpoint that returns a user plus a self link:
/**
* @Relation("self", route="api_user_show", routeParameters={"id" = "id"})
*/
public function getUserAction($id) { /* return User */ }
Laravel equivalent to build instead: a UserResource extends JsonResource returning ->additional(['_links' => ['self' => route('api.users.show', $this->id)]]). Same contract, no bundle.
Route-driven link generation. The bundle's core idea is that links are declared next to the action that serves them, using the route name plus an expression over request/entity data. This keeps URLs out of your entities and guarantees self links match real routes. The Laravel analogue is generating links from named routes inside JsonResource::toArray()/additional() rather than hardcoding paths.
Embedding sub-resources. @Embedded inlines related representations (e.g. a user's friends) so clients avoid extra round-trips; @Exclusion prunes fields. Combined with JMS serialization groups this is how you control payload shape per endpoint. In Laravel this maps to whenLoaded() / whenIncluded() plus explicit Relation::load() guards.
Programmatic links. Annotations are only sugar — the LinkFactory / LinkManager services can build Link objects in plain service code (e.g. a domain service producing a next link for a cursor). This is the extension point worth copying: a single LinkBuilder class in Laravel that everything funnels through, so relation names and URL shapes stay consistent.
Pagination links. The Pagerfanta integration emits first/last/next/prev for embedded collections — the pattern to replicate with Laravel's LengthAwarePaginator and $paginator->nextPageUrl().
A workable Laravel port:
abstract class HateoasResource extends JsonResource
{
protected array $links = [];
public function link(string $rel, string $url): static
{
$this->links[$rel] = ['href' => $url];
return $this;
}
public function toArray($request): array
{
return parent::toArray($request);
}
public function with($request): array
{
return ['_links' => $this->links];
}
}
Consistency beats cleverness. Whatever you build, keep rel values a closed set (self, collection, next, prev, related) documented in one place. Leaking internal route names as rel values is the single most common way this kind of layer rots.
fsc_hateoas metadata cache. Changing an annotation without warming/clearing that cache silently serves stale links — the Symfony equivalent of a Laravel config-cache surprise.route="api_user_show" becoming api_users_show produces a 500 at serialization, not at boot. Add assertions on _links keys in tests; this applies equally to Laravel's route() helper.http:// links in an https:// API. Configure trusted proxies/URL::forceScheme and test the absolute URL, not just the path.@Embedded chains are easy to declare and expensive to serve; N+1 creeps in through the JMS handler where your query log won't obviously point. Bound embedding with serialization groups and measure response size.href/routeParameters makes failures hard to trace. Keep expressions to simple property reads; move logic into the LinkFactory.fsc_hateoas config — pin it, document it, and version it. Clients break when a config tweak reflows the envelope.application/hal+json or JSON:API semantics, use a spec-compliant library (in Laravel: laravel-graphql-style dedicated packages, spatie/laravel-fractal, or LaravelJsonApi) — the bundle predates both specs' popular tooling.How can I help you explore Laravel packages today?