Weird Endpoint: Command: Power-Up a Programmer
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOn 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()
:
// ... 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:
// ... 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
:
// ... 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:
// ... 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:
// ... 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
:
// ... 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:
// ... 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:
// ... 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()
:
// ... 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
:
// ... 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:
// ... 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
:
// ... 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)
:
// ... 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!
REST for me felt always like waste of time thinking about correct urls, status codes. As you showed also - there are situations where it does not fit in REST rules anyway. I remember long time ago I was using only GET and POST and whatever url felt good for me. And it was enough. No http codes. And I had no problems.