This tutorial has a new version, check it out!

Subordinate URL Structure

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

When an API client fetches information about a programmer, they might also want a quick way to get details about all of the battles the programmer has fought. OK, we could add a link on programmer to an endpoint that returns all of that programmer's battles.

This collection of battles is called a subordinate resource because we're looking at the battles that belong to a programmer. It feels like a parent-child relationship. Truthfully, this whole idea of subordinate resources isn't that important - and it's usually subjective. But, if you're creating an endpoint and you realize that it feels like a subordinate resource, a few things usually change.

To start: how should we setup the URL? Is it /api/battles?username= or /api/battles/{nickname}? If you read up on REST API stuff, they'll tell you the URL structure never matters. Ok, let's use /hamburger! No, that's stupid... unless your app is about delicious hamburgers. For the rest of us, there are some sensible rules we should follow.

First, in Programmer, let's add a new link from the programmer to the battles for that programmer, and then we'll create that endpoint. For the rel, let's use battles:

... lines 1 - 10
* Programmer
... lines 14 - 16
* @Hateoas\Relation(
* "self",
* href=@Hateoas\Route(
* "api_programmers_show",
* parameters = { "nickname"= "expr(object.getNickname())" }
* )
* )
* @Hateoas\Relation(
* "battles",
... lines 26 - 29
* )
class Programmer
... lines 33 - 208

That could be anything: just be consistent. Whenever you link to a collection of battles, use battles.

Everything else looks good. The route will probably need the nickname of the programmer... we're not sure yet - because this endpoint doesn't exist. Let's create it.

The URL Structure

But wait! Which controller should it go into: ProgrammerController or BattleController? There's no right answer to this, but because these are battles for a specific programmer, the battle is subordinate to the programmer. In these situations, I tend to put the code in the parent resource's controller: ProgrammerController.

And actually, the biggest reason I do this is because of how we're going to structure the URL. Make a public function battlesListAction():

... lines 1 - 22
class ProgrammerController extends BaseController
... lines 25 - 153
public function battlesListAction()

Above that, add @Route() and the URL, which of course, could be anything. Make it /api/programmers/{nickname}/battles:

... lines 1 - 22
class ProgrammerController extends BaseController
... lines 25 - 150
* @Route("/api/programmers/{nickname}/battles", name="api_programmers_battles_list")
public function battlesListAction()

Check this out: the first three parts of the URL identify a specific programmer resource. Then, /battles looks almost like a battles property on programmer. That feels right... and that's all that matters.

For the name, use api_programmers_battles_list and copy that. Every part of this is consistent and almost self-documenting.

Head back to Programmer and paste the route name. The big lesson about subordinate resources is that it's OK to have them and that this is the best URL structure to use. But if some other organization feels better to you, do it. This is one of those REST topics you should not lose time thinking about.

Leave a comment!

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
        "willdurand/hateoas-bundle": "^1.1" // 1.1.1
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0