Symfony 4 REST API Part 1 – FOSRestBundle

REST as in Representational State Transfer is something that can’t be ignored in today’s ecosystem. There is a big need for creating Restful applications these days. This might be related to the continued rise of JavaScript and it’s single page applications. In this article we are going to explore the easiest way to implement a REST API in your Symfony application. And by doing this we are going to follow some good practices concerning this. If you want to create a truly great REST API. Then it is important to understand what REST actually means. It is not a fancy name for just returning some JSON data based on request. When you clearly understand and embrace Restfulness. You will create a better and more robust REST API.

What is REST?

A REST API should use HTTP as it was originally envisioned. This means the use of GET, POST, PUT and DELETE. To lookup data our REST API needs to use GET. For creation we use POST, for mutation of our data we use PUT and last but not least we use DELETE for deletion. Yes, creating a Restful application is making full use of HTTP.

So ANY application build in a Restful way is thus a client-server process. This means that a client requests something from the server. This can be trough a GET or POST request. And then gets back a response.

REST is stateless?

In this client-server process, rest is stateless. Stateless means that the server does not hold any state. There is no session data. The state should be kept on the client. This client needs to send all the data in the request, so the server can generate a correct response in a stateless way. When adding authentication to your Rest API. You need to send the authentication headers in each consecutive call.

All of this also means that our GET requests should be idempotent. Making the same request more then once has the same result as making the request only once. This can only be done when the server is stateless. An exception here is POST. If you make a POST more then once, then you create multiple objects on the server. All of this is important to keep in mind.

What is a Resource?

In REST we call our data representation a resource.

The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. “today’s weather in Los Angeles”), a collection of other resources, a non-virtual object (e.g. a person), and so on. In other words, any concept that might be the target of an author’s hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time.

A resource can be an item or a collection. Articles is a collection resource and Article is an item resource. A collection resource is identified (URI) by /articles while an item resource is identified as /articles/{articleId}.

It’s also important to know that a resource can have sub resources. Just as your Entity can have associations.
So for example comments for an article can be indentified as /articles/{articleId}/comments or /articles/{articleId}/comments/{commentId}.

Making correct use of the HTTP verbs and using this naming schema makes your API easy to use. There is nothing worse then an API that has confusing names and is hard to use.

Implementing FOSRestBundle

Now we got a bit of the theory behind REST. Its finally time that we are going to create a Restful API in Symfony. The FOSRestBundle is the de facto standard for this. It is not over engineered or simplified. It does what it needs to do. And that’s exactly what we need!

But… the current documentation for FOSRestBundle is a bit lack luster in my opinion. I’m sure they working on getting it updated for Symfony 4. But no worries, I’m going to get you started in no time. And to start I assume you have already got a Symfony 4 project up or running. Or you can start from the example I prepared here. I assume you have already got your Entity and Repository set up. (The example includes these.)

Lets start by installing the FOSRESTBundle. But before we can do that we need the Serializer Component to serialize or deserialize the resources of our application.

composer require symfony/serializer

And then we can to install the FOSRestBundle.

composer require friendsofsymfony/rest-bundle

Config

Now we will edit our config to have the following setup:

  • We want to separate the Rest controllers from our Web controllers
  • We want that create Rest controllers by using annotations
  • All our Rest calls should be prefixed by /api
#config/packages/fost_rest.yaml

fos_rest:
    view:
        view_response_listener:  true
    format_listener:
        rules:
            - { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] }
#config/routes/annotations.yaml
#Note: use your own resource/directory as you prefer

web_controller:
    resource: ../../src/Infrastructure/Controller/Web/
    type: annotation

rest_controller:
    resource: ../../src/Infrastructure/Controller/Rest/
    type: annotation
    prefix: /api

Now we are ready. We defined that all our REST actions are on /api. And we will be using the JSON format. We also separate our controllers between Web and Rest. It’s in my opinion a good practice to keep your API and Web controllers clearly separated.

Creating resource methods

Before we can start creating our resource methods. We need to create a controller in our Rest directory and make it extend the FOSRestController.

class ArticleController extends FOSRestController

Lets now find out how we are going to implement the basic HTTP verbs in our API.

POST

So the first HTTP verb we are going to implement is POST. We use POST to create a resource.

use FOS\RestBundle\Controller\Annotations as Rest;

    /**
     * Creates an Article resource
     * @Rest\Post("/articles")
     * @param Request $request
     * @return View
     */
    public function postArticle(Request $request): View
    {
        $article = new Article();
        $article->setTitle($request->get('title'));
        $article->setContent($request->get('content'));
        $this->articleRepository->save($article);

        // In case our POST was a success we need to return a 201 HTTP CREATED response
        return View::create($article, Response::HTTP_CREATED);
    }

As you can see we use the Rest\Post annotation. In the background this is the same as the @Route annotation. But it sets the method as POST. All these Rest annotation are just nice little shortcuts to the Route component.

You can test this by executing the following request:
POST /api/articles HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{"title": "Title", "content": "Content"}

As response we get back the created Article object.

GET

The next on the list is GET. As we know GET is idempotent. Every time the same URL to this method is called, we get back the same result. As a consequence GET calls can and will be cached. Unless there is some data change.

/**
 * Retrieves an Article resource
 * @Rest\Get("/articles/{articleId}")
 */
public function getArticle(int $articleId): View
{
    $article = $this->articleRepository->findById($articleId);

    // In case our GET was a success we need to return a 200 HTTP OK response with the request object
    return View::create($article, Response::HTTP_OK);
}

You can test this by executing the following request:
GET /api/articles/1 HTTP/1.1
Host: localhost:8080

As a response we get back the requested Article object.

GET collection

We can also do a GET collection call. The only difference here is that we don’t provide an ID in our request.

/**
 * Retrieves a collection of Article resource
 * @Rest\Get("/articles")
 */
public function getArticles(): View
{
    $articles = $this->articleRepository->findAll();

    // In case our GET was a success we need to return a 200 HTTP OK response with the collection of article object
    return View::create($articles, Response::HTTP_OK);
}

You can test this by executing the following request:
GET /api/articles HTTP/1.1
Host: localhost:8080

As a response we get back a collection of Articles.

PUT

The next one on the list is PUT. This is also impotent because we will always update the same object on every request. PUT is thus used to replace an object.

/**
 * Replaces Article resource
 * @Rest\Put("/articles/{articleId}")
 */
public function putArticle(int $articleId, Request $request): View
{
    $article = $this->articleRepository->findById($articleId);
    if ($article) {
        $article->setTitle($request->get('title'));
        $article->setContent($request->get('content'));
        $this->articleRepository->save($article);
    }

    // In case our PUT was a success we need to return a 200 HTTP OK response with the object as a result of PUT
    return View::create($article, Response::HTTP_OK);
}

You can test this by executing the following request:
PUT /api/articles/1 HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{"title": "Title", "content": "New content"}

As a response we get back the updated Article object.

DELETE

Last but not least is the DELETE verb. As the name suggests, this is used to remove a resource object.

/**
 * Removes the Article resource
 * @Rest\Delete("/articles/{articleId}")
 */
public function deleteArticle(int $articleId): View
{
    $article = $this->articleRepository->findById($articleId);
    if ($article) {
        $this->articleRepository->delete($article);
    }

    // In case our DELETE was a success we need to return a 204 HTTP NO CONTENT response. The object is deleted.
    return View::create([], Response::HTTP_NO_CONTENT);
}

You can test this by executing the following request:
DELETE /api/articles/1 HTTP/1.1
Host: localhost:8080

It’s important to remember that you are not required to provide all of these verbs for your resources. Only the ones you want a client to use.

Creating the Application Service

Now that we know how to create a basic Rest API. We should now work on improving it. First we are going to make our Rest controller a bit thinner.  We create an application service with methods that we will also use in our web controllers. This is a good example of DRY.

final class ArticleService
{

    /**
     * @var ArticleRepositoryInterface
     */
    private $articleRepository;


    public function __construct(ArticleRepositoryInterface $articleRepository){
        $this->articleRepository = $articleRepository;
    }

    public function getArticle(int $articleId): ?Article
    {
        return $this->articleRepository->findById($articleId);
    }

    public function getAllArticles(): ?array
    {
        return $this->articleRepository->findAll();
    }

    public function addArticle(string $title, string $content): Article
    {
        $article = new Article();
        $article->setTitle($title);
        $article->setContent($content);
        $this->articleRepository->save($article);

        return $article;
    }

    public function updateArticle(int $articleId, string $title, string $content): ?Article
    {
        $article = $this->articleRepository->findById($articleId);
        if (!$article) {
            return null;
        }
        $article->setTitle($title);
        $article->setContent($content);
        $this->articleRepository->save($article);

        return $article;
    }

    public function deleteArticle(int $articleId): void
    {
        $article = $this->articleRepository->findById($articleId);
        if ($article) {
            $this->articleRepository->delete($article);
        }
    }
    
}

And then we implement this service in our controllers:

final class ArticleController extends FOSRestController
{
    /**
     * @var ArticleService
     */
    private $articleService;

    /**
     * ArticleController constructor.
     * @param ArticleService $articleService
     */
    public function __construct(ArticleService $articleService)
    {
        $this->articleService = $articleService;
    }

    /**
     * Creates an Article resource
     * @Rest\Post("/articles")
     */
    public function postArticle(Request $request): View
    {
        $article = $this->articleService->addArticle($request->get('title'), $request->get('content'));

        // Todo: 400 response - Invalid Input
        // Todo: 404 response - Resource not found

        // In case our POST was a success we need to return a 201 HTTP CREATED response with the created object
        return View::create($article, Response::HTTP_CREATED);
    }

    /**
     * Retrieves an Article resource
     * @Rest\Get("/articles/{articleId}")
     */
    public function getArticle(int $articleId): View
    {
        $article = $this->articleService->getArticle($articleId);

        // Todo: 404 response - Resource not found

        // In case our GET was a success we need to return a 200 HTTP OK response with the request object
        return View::create($article, Response::HTTP_OK);
    }

    /**
     * Retrieves a collection of Article resource
     * @Rest\Get("/articles")
     */
    public function getArticles(): View
    {
        $articles = $this->articleService->getAllArticles();

        // In case our GET was a success we need to return a 200 HTTP OK response with the collection of article object
        return View::create($articles, Response::HTTP_OK);
    }

    /**
     * Replaces Article resource
     * @Rest\Put("/articles/{articleId}")
     */
    public function putArticle(int $articleId, Request $request): View
    {
        $article = $this->articleService->updateArticle($articleId, $request->get('title'), $request->get('content'));

        // Todo: 400 response - Invalid Input
        // Todo: 404 response - Resource not found

        // In case our PUT was a success we need to return a 200 HTTP OK response with the object as a result of PUT
        return View::create($article, Response::HTTP_OK);
    }

    /**
     * Removes the Article resource
     * @Rest\Delete("/articles/{articleId}")
     */
    public function deleteArticle(int $articleId): View
    {
        $this->articleService->deleteArticle($articleId);

        // Todo: 404 response - Resource not found

        // In case our DELETE was a success we need to return a 204 HTTP NO CONTENT response. The object is deleted.
        return View::create([], Response::HTTP_NO_CONTENT);
    }
}

If you are only building a Rest API and there is no situation you are going to use this code outside of your controller. You should be pragmatic and keep your code in the controller.

Handling Exceptions

You may ask; how do we correctly handle exceptions? Its important to understand the different kind of responses you can and should have it your Restful application. In the previous code block I added some Todo comments. In this section we are going to find out how we can create these responses and still keep our Application Service cleanly separated from our Rest Controller.

Resource Not Found

The first response we need to tackle is Resource Not Found. This happens we we are unable to find our Entity. We should always let our code fail fast. Our getArticle service method will also be used in our Web controller. So what we should do is throw an EntityNotFoundException. We can then handle this exception differently in both of our controllers.

public function getArticle(int $articleId): Article
{
    $article = $this->articleRepository->findById($articleId);
    if (!$article) {
        throw new EntityNotFoundException('Article with id '.$articleId.' does not exist!');
    }
}

If we do a GET request to an id that does not exist. We will now get a 500 Internal Server Error on the client. This is not a correct response.  We want to show a 404 not found exception.
We can do that by configuring this exception to return a 404 code and we also want to show our custom message. For this we can use the fos_rest exception controller.

#config/packages/fos_rest.yaml
...
    exception:
        exception_controller: 'fos_rest.exception.controller:showAction'
        codes:
            Doctrine\ORM\EntityNotFoundException: 404
...

If we now send a  request to GET /api/articles/99. We will get 404 Not Found response. This is exactly what we wanted. The client can now determine that when he gets a 404 response. That the resource does not exist.

Feel free to also add this exception to your DELETE and PUT methods. When we request a collection of articles. There is no need to throw an exception if nothing is found. We can just return empty array in that case.

Invalid Input

Now another situation might be that our title needs to be at least 5 characters long. We can also add an exception in our Entity in case this is not true.

/**
 * @param string $title
 * @throws \InvalidArgumentException
 */
public function setTitle(string $title): void
{
    if (\strlen($title) < 5) {
        throw new \InvalidArgumentException('Title needs to have more then 5 characters.');
    }

    $this->title = $title;
}

As we learned we can add a configuration option to handle this situation. Now we want that any Logic or Domain exception gives back a 400 Bad Request response instead of the default 500 Internal Server Error. So lets make that change.

exception:
 exception_controller: 'fos_rest.exception.controller:showAction'
 codes:
 Doctrine\ORM\EntityNotFoundException: 404
 \LogicException: 400
 \DomainException: 400
 messages:
 Doctrine\ORM\EntityNotFoundException: true
 \LogicException: true
 \DomainException: true

When we now try to add an article with less then 5 characters. We get back the following response.

{"code":400,"message":"Title needs to have more then 5 characters."}

The finished version can be found here.

Recap

So now we know what REST and Restful is. A restful API should be stateless. The URI’s for our resources should be named correctly to make our API easy to use by our clients. We know how to create a Restful application using HTTP verbs.  We learned how to implement this with the FOSRestbundle. For this we used some good practices and also show correct responses in case exceptions happen. All in all we now have a good understanding of REST and are ready to create Restful applications in a professional way.

Next week we will take a look at how to implement a Data Transfer Object.