Well hey guys! I've wanted to write this series for years, and now that it's here, I'm so pumped! That's because even though building an API can be really tough, the system we're about to build feels simple, and really a bit beautiful.

We have another REST series on the site where we build the API in Silex and learn the short list of REST concepts like resources, representations, what status codes to return, what headers to set, how to format your JSON and a few other buzzwords like hypermedia, HATEOAS and of course, don't forget about our favorite: idempotency.

But in this series, I'll assume you have a basic grasp of this stuff and we'll get straight to work. If you're confused by a term, head back to that series to fill in any gaps.

The Project

Ok, I've got the "start" directory for the project downloaded, I've configured parameters.yml and I've already run composer install. So let's launch the built-in web server:

php app/console server:start

Hey, it's Code Battles! This is the same awesome project we built in Silex for the other REST series. It already has a slick web interface - so we're going to build the API. To make sure we can login, let's create the database and load the fixtures:

php app/console doctrine:database:create
php app/console doctrine:schema:create
php app/console doctrine:fixtures:load

Now login with a fixtures user: weaverryan and the very secure password foo.

The Code Battles Web Interface

To understand the API we're going to build, let me give you a quick 60-second tour. And please keep your hands and arms inside the project at all times.

The first resource is a programmer, and we start by creating one. Give it a name, a clever tag line, choose one of the avatars and compile! Next, a programmer has energy, and you can change that by powering them up. Sometimes good things happen that give you power, sometimes bad things happen -- like a case of the Mondays.

With some power, you can start a battle. These are projects, and projects are the second resource. And when you select one, it creates our third resource: a battle. Our programmer killed it! Each battle is between one programmer resource and one project resource. On the homepage, you can see a list of all the battles our programmer has bravely fought.

POST to /api/programmers

So where do we start with the API? Well, other than logging in - which we'll talk about later - the first thing we do on the web is create the programmer. That's where we should start. Building an API is no different than building for the web: you need to step back and think about your user-flow and build things piece-by-piece in that order.

Open up app/config/routing.yml. I'm loading annotation routes from a Controller/Web sub-directory. I put all my web stuff there because now I can create an Api directory right next it and keep things organized.

In routing.yml, I'll keep two separate route imports: one for Web/ and I'll add a new one for Api/. Trust me - this will come in handy later:

8 lines app/config/routing.yml
resource: "@AppBundle/Controller/Web"
type: annotation
resource: "@AppBundle/Controller/Api"
type: annotation

Now create the new ProgrammerController - and make it extend Symfony's Controller like normal:

21 lines src/AppBundle/Controller/Api/ProgrammerController.php
namespace AppBundle\Controller\Api;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
... lines 6 - 9
class ProgrammerController extends Controller
... lines 12 - 19

Our first endpoint will be for creating Programmers, so let's start with public function newAction(). Above it, setup the @Route annotation with the URL /api/programmers. Let's also make it only respond to POST requests:

21 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 4
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
... lines 8 - 9
class ProgrammerController extends Controller
* @Route("/api/programmers")
* @Method("POST")
public function newAction()
... line 18

URL Structures and HTTP Methods

Ok, we just made 2 interesting architectural decisions:

First, we're going to start all our API URI's with /api. That's opinionated, and RESTfully speaking, it's wrong. REST says that if we want to return an HTML or JSON representation of a programmer resource, we should have just one URI - like /programmers/HappyCoderCat. This one URI should be able to return both formats based on a header the client sends.

If you want to do this, awesome - go for it! But it's not easy to do, and I'm not sure it's worth it. That's why we've separated the Web and Api stuff into different controllers and URIs. Now we can focus just on getting our API right.

The second architectural decision we made was to create a new resource by sending a POST request to that resource's collection URI - so /api/programmers. If you're curious why, watch our other screencast and learn about idempotency. And, in REST, you can make your URLs look however you want. But in practice, we're going to use a very consistent pattern. Because even though you can make your URLs super weird you probably shouldn't.

"Testing" the POST Endpoint

We'll return a new Response from the controller: Let's do this!

21 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 7
use Symfony\Component\HttpFoundation\Response;
... lines 9 - 11
* @Route("/api/programmers")
* @Method("POST")
public function newAction()
return new Response('Let\'s do this!');
... lines 20 - 21

Ok, so the easy days of just refreshing our browser to try this out are gone: we can't POST here directly in a browser. Now, a lot of people use Postman or something like it to test their API. And while it's great, I think there's a better way.

For now, create a new file - testing.php - right at the root of the project. Inside, require Composer's autoloader:

16 lines testing.php
require __DIR__.'/vendor/autoload.php';
... lines 4 - 16

We're going to use the Guzzle library to hit our new endpoint and make sure it's working. I already installed it into the project - so go directly to $client = new Client([]) and pass it some configuration:

16 lines testing.php
... lines 1 - 4
$client = new \GuzzleHttp\Client([
'base_url' => 'http://localhost:8000',
'defaults' => [
'exceptions' => false
... lines 11 - 16


To install this same version of Guzzle into your project, use Composer to fetch version 5.*:

composer require guzzlehttp/guzzle:~5.0

The first is base_url set to localhost:8000. Next, pass it a defaults key - these are options that'll be passed, by default, to each request. Set one option - exceptions - to false. Normally, if our server returns a 400 or 500 status code, Guzzle blows up with an Exception. This makes it act normal - it'll return a Response always. Trust me, that's nice!

Now make the request - $response = $client->post('/api/programmers'). Echo the $response - it's an object, but has a really pretty __toString method on it:

16 lines testing.php
... lines 1 - 11
$response = $client->post('/api/programmers');
echo $response;
echo "\n\n";

Try it by hitting this file from the command line:

php testing.php

Ok, let's fill in the guts and make this work!

Leave a comment!

  • 2017-01-20 Henri Tompodung

    Ups.. sorry my bad! >.<
    Thank you Victor Bocharsky

  • 2017-01-20 Victor Bocharsky

    Yo Henry,

    I think it was just a misprint :) Look closely to "GuzzleHtpp" - it should be "GuzzleHttp", i.e. "GuzzleHttp\Client".


  • 2017-01-20 Henri Tompodung

    Hi everyone.. I get this error:
    PHP Fatal error: Uncaught Error: Class 'GuzzleHtpp\Client' not found in /home/henri/myprojects/symfonyrest/testing.php:5
    Stack trace:
    #0 {main}
    thrown in /home/henri/myprojects/symfonyrest/testing.php on line 5

    and this is my testing.php :

    'defaults' => [
    'exceptions' => false,

    $response = $client->post('/api/programmers');

    echo $response;
    echo "\n\n";

    any suggestion?

  • 2017-01-13 Ryan Pardey

    I think that might have been it. I don't think the download is buggy. I am new to Symfony, so likely user error :P

  • 2017-01-12 weaverryan

    Yo Ryan!

    Hmm, I think you're absolutely right that you need version 1.2.5 at least of doctrine/annotations to avoid this problem. But, if you download the course code, the project ships with doctrine/annotations 1.2.7. Did you possibly start the project in some other way and have an older version of the library somehow? I just want to make sure that our code download doesn't have a bug for others!


  • 2017-01-10 Ryan Pardey

    I had tried that and it didn't work as I'm running php7. Ended up having to replace the Doctrine annotations directory with this:

    And followed a few other instructions here:

  • 2017-01-10 Victor Bocharsky

    Hey Ryan,

    Have you tried to add `opcache.load_comments=1` to your php.ini as it suggested?


  • 2017-01-10 Ryan Pardey

    I got this on composer install:

    You have to enable opcache.load_comments=1 or zend_optimizerplus.load_comments=1.

    Script Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::clearCache handling the post-install-cmd event terminated with an exception

    An error occurred when executing the "'cache:clear --no-warmup'" command.

    Any ideas?

  • 2016-12-20 emm

    Thank you pointing in the right direction! Although by just changing only the nginx/sites-enabled/ i had some troubles (404 errors) with testing env because i think the real path was "domain/app_dev.php/app_test.php/api/whatever" but Victors(knp) advice in video13/course1 helped me. i will try to manipulate paths from .htaccess from now on but so long all ok.

  • 2016-12-19 weaverryan

    Awesome! So, whether or not you need the app_dev.php is *all* about your web server :). Ultimately, if you want the "dev" environment to be executed, then the web/app_dev.php must be executed. As I mentioned earlier, if you use the built-in web server with Symfony, it pre-configures things so that you *don't* need the app_dev.php at the beginning of all of the URLs: if there is no file in the URL, then it is "rewritten" to use app_dev.php. If you use Apache, for example, you could setup rewrite rules locally to do the same. In fact, there is a web/.htaccess file that sets up rewrite rules in Apache so that app.php is used by default (which is what you want in production). I know a lot of people that setup their web server locally to automatically rewrite to app_dev.php so that they don't need to have it in their URL.


  • 2016-12-18 emm

    That helped me!! i couldn't navigate the website so i changed nginx to serve app_dev.php and its working. But isn't there a way to prepend all routes with /app_dev.php within symfony?


  • 2016-10-06 weaverryan

    Hey Maksym!

    We have it because it's still relevant (the changes from Symfony 2 to 3 - other than the directory structure changes - are quite minor). We have a few other things on the Symfony 3 track that are done with later versions of Symfony 2, but are still relevant. And in this case, we *do* switch to Symfony 3 during Course 4 - so the series is a bit mixed!


  • 2016-10-06 Maksym Minenko

    Why is this on the Symfony *3* track, I wonder?

  • 2016-09-07 Victor Bocharsky

    Hey Prakash,

    Everything is mostly the same. First of all you need to upgrade composer dependencies according to the Symfony 3. To do that, you should ensure that bundles you use in project support Symfony 3.x, check its composer.json file to see symfony/framework-bundle: ^2.0 || ^3.0. If you see "^3.0" - it should work and you can do upgrade.
    P.S. Don't forget to fix all deprecations in 2.8 before upgrading. We have a tutorial about it:
    How to Upgrade to Symfony 2.8, then 3.0


  • 2016-09-06 Prakash

    Hello how i do the REST API with syfmony 3.* version. currently example is with symfony 2.6. how i can migtae to syfmony 3?

  • 2016-08-23 weaverryan

    Hey Andjii!

    The tutorial uses guzzle/guzzle version 3.7, not guzzlehttp/guzzle at version 6 (which I'm guessing you are using). You can use the old, guzzle/guzzle just fine, or use the new library. But in that case, you'll need to do a little bit of translating for the new version of the library. Check out this comment for a few more details, including a gist with a version of testing.php that should work with the latest version :).

    But, to finally answer your question, you can't just echo the $response in Guzzle 6 (as you know), but you *can* print the body:

    echo $response->getBody();

    In some ways, the new version of the library is not as user-friendly as the old version. You can also check out this Response debugging function that we use in the Symfony REST tutorial:


  • 2016-08-23 Andjii

    Hello! I get the following error: Catchable fatal error: Object of class GuzzleHttp\Psr7\Response could not be converted to string in \testing.php on line 37. there is "echo $response". how should I convert my response object to string?

  • 2016-08-18 weaverryan

    Hey John!

    Yep - there are a few libraries that still work fine, but are older versions at this point. The only one that I know is actually *deprecated* is Guzzle. We use guzzle/guzzle at version 3, but the latest version os guzzlehttp/guzzle (yes it has a different name now) at version 6! We don't technically use this library for the API... but we do use it for *testing* the API. In the Symfony REST tutorial - starting on episode 4 ( we use the latest and greatest version 6. So, if you're curious about the differences, you can compare. Otherwise, if you're here for the API stuff, I wouldn't worry about it :).

    About the 404 on the Font file, I'm not sure about that. If you download the starting code, there *is* a web/vendor/font-awesome/fonts/fontawesome-webfont.ttf file in the project - so it should be loading this just fine. It shouldn't affect anything either way (you might just be missing some cute icons) but the 404 is mysterious!


  • 2016-08-17 John

    When I did a composer install, I did receive a ton of deprecation notices. But the app runs except in the browser console it says GET http://localhost:8000/vendor/font-awesome/fonts/fontawesome-webfont.ttf?v=4.0.3 404 (Not Found)

  • 2016-06-09 weaverryan

    Good find Vlad! That is a pretty subtle difference between the old and new version of Guzzle. Thanks for sharing!

  • 2016-06-08 Vlad

    Thanks Ryan! Changing base_url to base_uri fixed the issue for me. I'm using Guzzle 6.

  • 2016-05-18 weaverryan

    Nice catch!

  • 2016-05-18 weaverryan

    Hi Roberto!

    Actually, I would argue that testing.php is not better than Postman :). However, we will soon (in the next chapters in this tutorial) take the code from testing.php and turn them into true functional tests for our API. I believe this *is* better than Postman, because we now have a test suite that we can run at any time.


  • 2016-05-16 Thierno Diop

    oki its good it was because in scurity.yml the A in api was writen in capital

  • 2016-05-16 Thierno Diop

    when i execute the testing.php file i am redirected to the login page is it normal ?

  • 2016-05-13 Roberto Briones Arg├╝elles

    Why is better option to use the `testing.php` file instead Postman? Seems dirty to me.

  • 2016-03-24 cool

    im sorry but i have to say this, her voice is too sweet for the tutorial. it almost put me to sleep

  • 2016-03-16 weaverryan

    Hi Scott!

    Because you're using Symfony, if you use the built-in web server command from Symfony (php app/console server:run) it will start a built-in web server where it *defaults* to using app_dev.php - without you needing to have it in the URL. Alternatively, you can setup any other web server locally to have the same behavior.

    BUT, more generally, we do talk about this in our Symfony REST tutorial. In our tests, we use a hook into Guzzle that automatically prepends the URL: In episode 4 (which I'm recording right now), I've updated the code that hooks into Guzzle to work for Guzzle version 6:

    I hope that helps - good question!

  • 2016-03-15 Scott Gutman

    In my testing.php i had to make the line $response = $client->post('/app_dev.php/api/programmers'); How do I change that?

  • 2016-01-17 Shairyar Baig

    Hi, I ended up using the version mentioned in tutorial tip and that fixed the problem.

  • 2016-01-16 weaverryan

    Hey Shairyar!

    Ah, the infamous "malformed URL" error - I get this a lot in my day-to-day work. Usually, it's because I've misconfigured the "base_uri" setting - so that when I request "/api/programmers", it literally tries to request this URI, instead of http://localhost:8000/api/programmers. Double check how you're setting the base_uri for whatever Guzzle version you're on. In Guzzle 5, I think the setting was base_url, and in 6, it's base_uri. Guzzle has been a tricky library lately, as many things have changed quickly.


  • 2016-01-14 Shairyar Baig

    I am stuck at the same problem and the updated testing.php does not work either, if it helps this is the full error I am getting

    PHP Fatal error: Uncaught exception 'GuzzleHttp\Ring\Exception\RingException' with message 'cURL error 3: <url> malformed' in /Users/shairyar/Sites/REST/vendor/guzzlehttp/ringphp/src/Client/CurlFactory.php:127
    Stack trace:
    #0 /Users/shairyar/Sites/REST/vendor/guzzlehttp/ringphp/src/Client/CurlFactory.php(91): GuzzleHttp\Ring\Client\CurlFactory::createErrorResponse(Array, Array, Array)
    #1 /Users/shairyar/Sites/REST/vendor/guzzlehttp/ringphp/src/Client/CurlHandler.php(96): GuzzleHttp\Ring\Client\CurlFactory::createResponse(Array, Array, Array, Array, Resource id #78)
    #2 /Users/shairyar/Sites/REST/vendor/guzzlehttp/ringphp/src/Client/CurlHandler.php(68): GuzzleHttp\Ring\Client\CurlHandler->_invokeAsArray(Array)
    #3 /Users/shairyar/Sites/REST/vendor/guzzlehttp/ringphp/src/Client/Middleware.php(54): GuzzleHttp\Ring\Client\CurlHandler->__invoke(Array)
    #4 /Users/shairyar/Sites/REST/vendor/guzzlehttp/ringphp/src/Client/Middleware.php(30): GuzzleHttp\Ring\Client\Middleware::GuzzleHttp\Ring\Client\{closure}(Array)
    #5 /Users/shairyar/Sit in /Users/shairyar/Sites/REST/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php on line 51

    Fatal error: Uncaught exception 'GuzzleHttp\Ring\Exception\RingException' with message 'cURL error 3: <url> malformed' in /Users/shairyar/Sites/REST/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php on line 51

    GuzzleHttp\Exception\RequestException: cURL error 3: <url> malformed in /Users/shairyar/Sites/REST/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php on line 51

    Call Stack:
    0.0001 225136 1. {main}() /Users/shairyar/Sites/REST/testing.php:0
    0.0071 1314064 2. GuzzleHttp\Client->post() /Users/shairyar/Sites/REST/testing.php:21
    0.0092 1683720 3. GuzzleHttp\Client->send() /Users/shairyar/Sites/REST/vendor/guzzlehttp/guzzle/src/Client.php:150

  • 2015-11-17 Noah Glaser

    I did the http_errors is false and it worked for me. Thank you
    'http_errors' => false

  • 2015-10-09 weaverryan

    Yo Azeem!

    I think you may be using a newer version of Guzzle than I am in the tutorial - I'm using 3.7 (which is quite old at this time). If you use something newer, you'll just need to translate a bit of the code. The full testing.php for the newest version of Guzzle would look something like this: Warning - I just hacked that together, it's probably not 100% perfect :).

    Or, you can use the 3.7 version - definitely the way to go if you just want to code through and learn the REST stuff.


  • 2015-10-08 Azeem

    getting this error when I run testing.php

    Uncaught exception 'GuzzleHttp\Exception\RequestException' with message 'cURL error 3: <url> malformed (see in /Users/amichael/Public/www/justlikeme/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php:187