Buy

Weird Endpoint: Command: Power-Up a Programmer

On our web interface, if you select a programmer, you can start a battle, or you can hit this "Power Up" button. Sometimes our power goes up, sometimes it goes down. And isn't that just like life.

The higher the programmer's power level, the more likely they will win future battles.

Notice: all we need to do is click one button: Power Up. We don't fill in a box with the desired power level and hit submit, we just "Power Up"! And that makes this a weird endpoint to build for our API.

Why? Basically, it doesn't easily fit into REST. We're not sending or editing a resource. No, we're more issuing a command: "Power Up!".

Let's design this in a test: public function testPowerUp():

351 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
... lines 336 - 348
}
}

Grab the $programmer and Response lines from above, but replace tagLine with a powerLevel set to 10:

351 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
$this->createProgrammer(array(
'nickname' => 'UnitTester',
'avatarNumber' => 3,
'powerLevel' => 10
));
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 345 - 348
}
}

Now we know that the programmer starts with this amount of power.

The URL Structure of a Command

From here, we have two decisions to make: what the URL should look like and what HTTP method to use. Well, we're issuing a command for a specific programmer, so make the URL /api/programmers/UnitTester/powerup:

351 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
... lines 336 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
... line 343
]);
... lines 345 - 348
}
}

Here's where things get ugly. This is a new URI... so philosophically, this represents a new resource. Following what we did with the tag line, we should think of this as the "power up" resource. So, are we editing the "power up" resource... or are we doing something different?

The "Power Up?" Resource???

Are you confused? I'm kind of confused. It just doesn't make sense to talk about some "power up" resource. "Power up" is not a resource, even though the rules of REST want it to be. We just had to create some URL... and this made sense.

So if this isn't a resource, how do we decide whether to use PUT or POST? Here's the key: when REST falls apart and your endpoint doesn't fit into it anymore, use POST.

POST for Weird Endpoints

Earlier, we talked about how PUT is idempotent, meaning if you make the same request 10 times, it has the same effect as if you made it just once. POST is not idempotent: if you make a request 10 times, each request may have additional side effects.

Usually, this is how we decide between POST and PUT. And it fits here! The "power up" endpoint is not idempotent: hence POST.

But wait! Things are not that simple. Here's the rule I want you to follow. If you're building an endpoint that fits into the rules of REST: choose between POST and PUT by asking yourself if it is idempotent.

But, if your endpoint does not fit into REST - like this one - always use POST. So even if the "power up" endpoint were idempotent, I would use POST. In reality, a PUT endpoint must be idempotent, but a POST endpoint is allowed to be either.

So, use ->post(). And now, remove the body: we are not sending any data. This is why POST fits better: we're not really updating a resource:

351 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
... lines 336 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 345 - 348
}
}

And the Endpoint Returns....?

Assert that 200 matches the status code:

351 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(200, $response->getStatusCode());
... lines 346 - 351

And now, what should the endpoint return?

We're not in a normal REST API situation, so it matters less. You could return nothing, or you could return the power level. But to be as predictable as possible, let's return the entire programmer resource. Read the new power level from this with $this->asserter()->readResponseProperty() and look for powerLevel:

351 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(200, $response->getStatusCode());
$powerLevel = $this->asserter()
->readResponseProperty($response, 'powerLevel');
... lines 348 - 351

This is a property that we're exposing:

208 lines src/AppBundle/Entity/Programmer.php
... lines 1 - 31
class Programmer
{
... lines 34 - 67
/**
... lines 69 - 71
* @Serializer\Expose
*/
private $powerLevel = 0;
... lines 75 - 206
}

We don't know what this value will be, but it should change. Use assertNotEquals() to make sure the new powerLevel is no longer 10:

351 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(200, $response->getStatusCode());
$powerLevel = $this->asserter()
->readResponseProperty($response, 'powerLevel');
$this->assertNotEquals(10, $powerLevel, 'The level should change');
... lines 349 - 351

Implement the Endpoint

Figuring out the URL and HTTP method was the hard part. Let's finish this. In ProgrammerController, add a new public function powerUpAction():

196 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
... lines 190 - 193
}
}

Add a route with /api/programmers/{nickname}/powerup and an @Method set to POST:

196 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 183
/**
* @Route("/api/programmers/{nickname}/powerup")
* @Method("POST")
*/
public function powerUpAction(Programmer $programmer)
{
... lines 190 - 193
}
}

Once again, type-hint the Programmer argument:

196 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 7
use AppBundle\Entity\Programmer;
... lines 9 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
... lines 190 - 193
}
}

To power up, we have a service already made for this. Just say: $this->get('battle.power_manager') ->powerUp() and pass it the $programmer:

196 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
$this->get('battle.power_manager')
->powerUp($programmer);
... lines 192 - 193
}
}

That takes care of everything. Now, return $this->createApiResponse($programmer):

196 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
$this->get('battle.power_manager')
->powerUp($programmer);
return $this->createApiResponse($programmer);
}
}

Done! Copy the testPowerUp() method name and run that test:

./vendor/bin/phpunit -—filter testPowerUp

Success!

And that's it - that's everything. I really hope this course will save you from some frustrations that I had. Ultimately, don't over-think things, add links when they're helpful and build your API for whoever will actually use it.

Ok guys - seeya next time!

Leave a comment!

  • 2016-11-11 Victor Bocharsky

    Hey Till,

    Yes, the "ProgrammerController::newAction()" shouldn't be available for anonymous users, otherwise we will have a problem, since in "Programmer::setUser()" has the "public function setUser(User $user)" signature, i.e. we don't allow passing null here. However, "@Security("is_granted('ROLE_USER')")" annotation for the "ProgrammerController" class ensure that we always has a user in this controller, so if user is null - you can't get these end points.

    Cheers!

  • 2016-11-11 Till

    Hi, thanks for this course. Shouldn't in real life the ProgrammerController->newAction better be available for anonymous? I you change this only by annotations. You get stuck with $this->getUser() which can't be used anonymously.

  • 2016-10-10 Johan

    That makes sense, thanks! :)

  • 2016-10-10 Victor Bocharsky

    Yeah, I'm definitely +1 for it! We always try to do that if it possible - it reduces misprints. But here we left it as is to reduce complexity and do not produce questions: some parts of the class is hidden due to our dynamic code blocks.

    Cheers!

  • 2016-10-10 Johan

    Ye you're right. I checked the symfony docs and they say the same thing. Thanks :)

    But what about we put @Route("/api/programmers") as a class annotation in the ProgrammerController and create routes relative to that route instead? I think it would be good :)

    Thanks Victor

  • 2016-10-10 Victor Bocharsky

    Hey Johan,

    But it's a good practice to hardcode URLs in tests. It's a bad idea to use router to generate URLs in tests. Imagine, you've accidentally changed some URL - then some tests will fail and show you the problem. of course you can move duplicated URL prefix in a private property, but probably it just worse test readability: tests should be well readable. And btw, URLs change very rare, i'd say API URLs never change ;)

    Cheers!

  • 2016-10-09 Johan

    The only problem I'm having with all this code right now is that I can't change the URL structure. If I wanted to use singular resource names, so "api/programmer" instead of "api/programmers" I'd be in trouble... I'd have to change all the tests and all the annotations in the controllers :(

  • 2016-06-18 weaverryan

    Oh, except that we *may* do a course on documentation - that is one topic that we haven't covered yet.

  • 2016-06-18 weaverryan

    Right now, no - I don't see enough topics that we still haven't covered. But, if you have some topics that are still troubling you, then you're probably not the only one, and there might be a course 6 :)

  • 2016-06-16 Vlad

    Will there be a REST Course 6?