Skip to main content

Symfony API Documentation

How to Have Good API Documentation in Symfony

How to Have Good API Documentation in Symfony

API documentation is one of those things every developer understands is important, but it’s also quite easy to put off until “later”, which (let’s be honest) might never actually happen. Whether you’re building a project solo or as part of a team, good documentation is a lifesaver for both your current self and anyone who might (or will!) work with your API in the future.

Why Good API Documentation Matters

Let’s set the stage: you’re building an API backend with Symfony, and there’s a frontend team eagerly waiting to hook into your endpoints. Without clear, accessible documentation, every conversation becomes an ad-hoc Q&A session. Details get lost, mistakes creep in, and for every new teammate, the onboarding cost gets higher and higher. Good API docs are like a friendly guide—empowering your frontend team to work independently and confidently.

But Doesn't Symfony "Just Solve" This?

If you’re using Symfony, you might have noticed it gently nudges you towards API Platform. And honestly, API Platform is a fantastic tool. It comes with a ton of power: automatic Swagger docs, serialization, validation, scaffolding—you name it. But here’s the catch: it brings along a lot of dependencies, including Doctrine. Maybe your project is simple and doesn’t need these extra complexities, or you want to stay lightweight for performance or maintenance reasons.

That’s where NelmioApiDocBundle comes in—and why it’s been my favorite way to document APIs in Symfony projects that don’t need the full power (and overhead) of API Platform.

Enter NelmioApiDocBundle: Lightweight OpenAPI Documentation

NelmioApiDocBundle is a tried-and-tested Symfony bundle that generates beautiful OpenAPI (Swagger) documentation for your REST APIs. It requires only minimal setup, doesn’t force Doctrine or heavy dependencies on your project, and still gives you all the Swagger UI goodness developers love.

Let’s Walk Through a Simple Example

Suppose we have a project with a basic Book model and a controller that returns a list of books. Here’s what it looks like without any documentation:

<?php
// src/Model/Book.php
namespace App\Model;

class Book
{
    private string $title;
    private string $author;
    private string $isbn;

    public function __construct(string $title, string $author, string $isbn)
    {
        $this->title = $title;
        $this->author = $author;
        $this->isbn = $isbn;
    }

    // ... getters ...
}
<?php
// src/Controller/BookController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\BookRepository;

class BookController extends AbstractController
{
    #[Route('/api/books', name: 'api_books')]
    public function index(BookRepository $bookRepository): Response
    {
        $books = $bookRepository->findAll();
        return $this->json($books);
    }
}

This works, but… anyone who wants to use the /api/books endpoint has to either read your code or ping you in chat. Not ideal!

Step 1: Install NelmioApiDocBundle

Let’s get NelmioApiDocBundle installed:

composer require nelmio/api-doc-bundle twig asset

This adds the bundle and ensures Twig and Asset components are present to serve the Swagger UI.

Step 2: Enable the API Documentation Routes

Add the following in ./config/routes/nelmio_api_doc.yaml:

app.swagger:
    path: /api/doc.json
    methods: GET
    defaults: { _controller: nelmio_api_doc.controller.swagger }

app.swagger_ui:
    path: /api/doc
    methods: GET
    defaults: { _controller: nelmio_api_doc.controller.swagger_ui }

What does this do? You now have two new routes: /api/doc (the graphical Swagger UI) and /api/doc.json (the raw OpenAPI spec).

Step 3: Annotate Your Models and Controllers

This is where the magic happens! By using PHP 8+ attributes (annotations), we attach OpenAPI documentation directly to our models and controller actions. First, update Book.php:

<?php
namespace App\Model;
use OpenApi\Attributes as OA;

class Book
{
    #[OA\Property(description: 'The title of the book', type: 'string', example: 'The Hitchhiker\'s Guide to the Galaxy')]
    public string $title;

    #[OA\Property(description: 'The author of the book', type: 'string', example: 'Douglas Adams')]
    public string $author;

    #[OA\Property(description: 'The International Standard Book Number', type: 'string', example: '978-0345391803')]
    public string $isbn;

    // ... constructor and getters ...
}

Notice how each property is now described for API consumers: what it is, its type, and an example value.

Next, let’s update the controller method to document the endpoint:

<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\BookRepository;
use App\Model\Book;
use OpenApi\Attributes as OA;
use Nelmio\ApiDocBundle\Attribute\Model;

class BookController extends AbstractController
{
    /**
     * List all books.
     */
    #[Route('/api/books', name: 'api_books', methods: ['GET'])]
    #[OA\Response(
        response: 200,
        description: 'Returns the list of books',
        content: new OA\JsonContent(
            type: 'array',
            items: new OA\Items(ref: new Model(type: Book::class))
        )
    )]
    #[OA\Tag(name: 'Books')]
    public function index(BookRepository $bookRepository): Response
    {
        $books = $bookRepository->findAll();
        return $this->json($books);
    }
}

This setup tells Nelmio/OpenAPI: “the response here is an array of Book objects, look at this model for field definitions.” You can group endpoints into logical tags (like “Books”) so your API docs stay organized.

What You'll See (and Why It’s Awesome)

  • Swagger UI at /api/doc: A clickable, browsable API interface showing all endpoints, expected request and response bodies, error codes, and data models.
  • Frontend Teams Love You: Complete, up-to-date docs generated directly from your code—with zero extra manual work. Less back-and-forth, more self-service, fewer headaches!
  • No Extra Weight: No need to drag in Doctrine (if you don’t want it), no overengineering. Just what you need, nothing more.

Tips & Good Practices

  • Keep Annotations Up to Date: As your API evolves, so should your docs. Teach your team to update annotations as part of new features (or PR checklists).
  • Describe Edge Cases: Don’t hesitate to annotate status codes, validation errors, or security constraints.
  • Use Example Payloads: Concrete examples help consumers understand what to send and expect.
  • Add Meta Info in nelmio_api_doc.yaml: You can configure API title, description, contact info, and more, making your docs look even more polished.

Wrapping Up

Robust, discoverable API documentation shouldn’t be an afterthought—and with NelmioApiDocBundle in Symfony, it doesn’t have to be. It’s simple, fast, and will keep both your backend and frontend teams happy and productive.
Next time you spin up a new API in Symfony, give NelmioApiDocBundle a try. Your future self (and your teammates) will thank you!

Happy documenting! 🚀

Popular posts from this blog

npm run build base-href

Using NPM to specify base-href When building an Angular application, people usually use "ng" and pass arguments to that invocation. Typically, when wanting to hard code "base-href" in "index.html", one will issue: ng build --base-href='https://ngx.rktmb.org/foo' I used to build my angular apps through Bamboo or Jenkins and they have a "npm" plugin. I got the habit to build the application with "npm run build" before deploying it. But the development team once asked me to set the "--base-href='https://ngx.rktmb.org/foo'" parameter. npm run build --base-href='https://ngx.rktmb.org/foo did not set the base href in indext.html After looking for a while, I found https://github.com/angular/angular-cli/issues/13560 where it says: You need to use −− to pass arguments to npm scripts. This did the job! The command to issue is then: npm run build -- --base-href='https://ngx.rktmb.org/foo...

wget maven ntlm proxy

How to make wget, curl and Maven download behind an NTLM Proxy Working on CentOS, behind an NTLM proxy: yum can deal without problem with a NTLM Proxy wget, curl and Maven cannot The solution is to use " cntlm ". " cntlm " is a NTLM client for proxies requiring NTLM authentication. How it works Install "cntlm" Configure "cntlm"  by giving it your credentials by giving it the NTLM Proxy Start "cntlm" deamon (it listens to "127.0.0.1:3128") Configure wget, curl and Maven to use "cntlm" instead of using directly the NTLM Proxy Note: You will have then a kind of 2 stages Proxy : cntlm + the NTLM proxy Configure CNTLM After installing cntlm, the configuration file is in "cntlm.conf". You must have your domain (in the Windows meaning), proxy login and  proxy password. Mine are respectively: rktmb.org, mihamina, 1234abcd (yes, just for the example) You must have you NTLM Proxy Hostnama or IP ...

Undefined global vim

Defining vim as global outside of Neovim When developing plugins for Neovim, particularly in Lua, developers often encounter the "Undefined global vim" warning. This warning can be a nuisance and disrupt the development workflow. However, there is a straightforward solution to this problem by configuring the Lua Language Server Protocol (LSP) to recognize 'vim' as a global variable. Getting "Undefined global vim" warning when developing Neovim plugin While developing Neovim plugins using Lua, the Lua language server might not recognize the 'vim' namespace by default. This leads to warnings about 'vim' being an undefined global variable. These warnings are not just annoying but can also clutter the development environment with unnecessary alerts, potentially hiding other important warnings or errors. Defining vim as global in Lua LSP configuration to get rid of the warning To resolve the "Undefined global vi...