Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

A Homepage for your API?

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

If I tell you to go to a webpage, I'll probably tell you to go to its homepage - like KnpUniversity.com - because I know once you get there, you'll be able see links, follow them and find whatever you need. And if you think about it, there's no reason an API has to be any different. And this is an idea that's catching on.

So right now, if I go to just /api in the Hal browser we get a 404 response, because we haven't built anything for this yet. I'm thinking, why not build a homepage that people can go to and get information about how to use our API?

API Homepage: Start Simple

I'll build this in ProgrammerController just for convenience. Let's add the new /api route and point it at a new method called homepageAction:

... lines 1 - 17
class ProgrammerController extends BaseController
{
protected function addRoutes(ControllerCollection $controllers)
{
// the homepage - put in this controller for simplicity
$controllers->get('api', array($this, 'homepageAction'));
... lines 24 - 41
}
... lines 43 - 185
}

Inside of here, I'm not going to return anything yet - let's just reuse that same createApiResponse, and I'll pass it an empty array:

... lines 1 - 43
public function homepageAction()
{
return $this->createApiResponse(array());
}
... lines 48 - 187

If we try this, we get an empty response, but it has a valid application/hal+json. So that's all we really need to do to get an endpoint working.

A Model Class for Every Resource

We know that every URL is a resource in the philosophical sense: /api/programmers is a collection resource and /api/programmers/Fred represents a programmer resource. And really, the /api endpoint is no different. By the way, I did not mean to leave the / off of my path - it doesn't matter if you have it, but I'll add it for consistency.

... lines 1 - 19
protected function addRoutes(ControllerCollection $controllers)
{
// the homepage - put in this controller for simplicity
$controllers->get('/api', array($this, 'homepageAction'));
... lines 24 - 41
}
... lines 43 - 187

So far, every time we have a resource, we have a model class for it. Why not do the same thing for the Homepage resource? Create a new class called Homepage:

<?php
namespace KnpU\CodeBattle\Model;
... lines 4 - 5
/**
* A model to represent the homepage resource
*
... lines 10 - 15
*/
class Homepage
{
}

And without doing anything else, I'll create a new object called $homepage. Don't forget to add your use statement whenever you reference a new class in a file. And instead of the empty array, we'll pass createApiResponse() the Homepage object:

... lines 1 - 45
public function homepageAction()
{
$homepage = new Homepage();
return $this->createApiResponse($homepage);
}
... lines 52 - 191

So every time we have a resource, we have a class for it. It doesn't really matter if the class is being pulled from the database, being created manually or being populated with data from something like Elastic Search.

If we hit Go on the browser, we get the exact same response back: no difference yet. But now that we have a model class, we can start adding things to it. And since every resource has a self link, let's add that to Homepage too. I'll grab the Relation from programmer for convenience. And of course, grab the use statement for it: I know I'm repeating myself over and over again!

Now, we need the name of the route. So let's go give this new route a name: api_homepage:

... lines 1 - 20
protected function addRoutes(ControllerCollection $controllers)
{
... line 23
$controllers->get('/api', array($this, 'homepageAction'))
->bind('api_homepage');
... lines 26 - 43
}
... lines 45 - 191

We'll use this in the Relation. And because there aren't any wildcards in the route, we don't need any parameters.

... lines 1 - 4
use Hateoas\Configuration\Annotation as Hateoas;
/**
* A model to represent the homepage resource
*
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "api_homepage"
* )
* )
*/
class Homepage
{
}

Cool!

Let's try this out! We have a link! Now, I know this doesn't seem very useful yet, because we have an API homepage that's linking to itself, but now if we wanted to, we could link back to the homepage from other resources. So if we were on the /api/programmers resource, we could add a link back to the homepage. When an API client GET's that URL, they'll see that there's a place to get more information about the entire API.

When you look at the Links section, there are a few other columns like title, name/index and docs. One of the things that this is highlighting is that your links can have more than just that href. So in Homepage, let's give the link a title of "The API Homepage":

... lines 1 - 6
/**
* A model to represent the homepage resource
*
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "api_homepage"
* ),
* attributes = {"title": "Your API starting point" }
* )
*/
class Homepage
... lines 19 - 21

Let's go back to the browser to see that title. And now we can give any link a little bit more information.

Hi, Welcome! Homepage Message

Let's keep going! Since this is the API homepage, you probably want to give the client a nice welcome message and maybe even link to the human-readable documentation. Even though this Homepage class isn't being pulled from the database doesn't mean that we can't have properties. Let's create a $message property and set that to some hard-coded text. You could even put your documentation URL here:

... lines 1 - 17
class Homepage
{
private $message = 'Welcome to the CodeBattles API! Look around at the _links to browse the API. And have a crazy-cool day.';
}

And now, our API homepage is getting interesting! But the real purpose of this homepage is to have links to the actual resources that the API client will want. This is really easy as well. The most obvious resource the client may want is the programmers collection. So let's do this here. We'll say programmers and we'll link to /api/programmers. That route doesn't have a name yet, so let's call it api_programmers_list:

... lines 1 - 20
protected function addRoutes(ControllerCollection $controllers)
{
... lines 23 - 31
$controllers->get('/api/programmers', array($this, 'listAction'))
->bind('api_programmers_list');
... lines 34 - 44
}
... lines 46 - 192

And now we can use it in the Relation. To be super friendly, we'll give this link a nice title as well:

... lines 1 - 6
/**
... lines 8 - 16
* @Hateoas\Relation(
* "programmers",
* href = @Hateoas\Route(
* "api_programmers_list"
* ),
* attributes = {"title": "The list of all programmers" }
* )
*/
class Homepage
... lines 26 - 29

So let's hit Go. Now we have a really great homepage. We can come here, we can see the message with a link to the documentation, we can visit all of the programmers, and now we're dangerous. From there we can follow links to a programmer, to that programmer's battles, and anything else our links lead us to.

Leave a comment!

3
Login or Register to join the conversation
Cameron Avatar
Cameron Avatar Cameron | posted 2 years ago

Hey folks, what is the purpose of hateoas and it's links? Can it be used to auto generate a graphQL schema? Do it have other uses? Because a Dev can simply look at the documentation to understand what endpoints to access.

And probably just as importantly: what situations is it unnessacary to add hateoas? For instance, if I'm the only consumer of the API and I'm already familiar with it, would it be nessacary to spend time to add hateoas?

Reply

Hey @cameron!

Excellent question :).

The long answer is that some people believe that these formats and links will become so standardized, that you could point an api client at the homepage of an api, and it would organically crawl the endpoints and follow the links until it found what it’s looking for (like a user browsing a web site).

But that’s a bit “science fiction” to me ;). Especially if you’re building an API for yourself.

But they are still really useful - especially the IRI links that api platform uses. Basically, I see links as a useful way to help pass more info to my javascript so that I don’t need to hardcode url patterns there. For example, if I make a GET request to /api/cheeses and then I want to fetch page 2 of the results, I don’t need to even care that we do this by adding ?page=2 (and that’s especially cool to not worry about if I already have other filters on the url like ?title=foo). Nope, I can just read the “next” link from the response and follow it. That’s not an “earth shatteringly cool thing”... but it *is* kinda nice.

The same is true for the IRI strings. If I fetch a cheese listing and I want more info about its owner, I know I can make a GET request to the owner IRI string to do that.

So... links are helpful! Done well, they make our life easier. But they’re not any crazy, magic solution.

Cheers!

1 Reply
Cameron Avatar

Thanks. it would be good to have this context prior to beginning the "how to" component of hateoas.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4", // v2.7.3
        "jms/serializer": "~0.16", // 0.16.0
        "willdurand/hateoas": "~2.3" // v2.3.0
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice