Pragmatic architecture 5: Documentation of the API

Dev Diary

In the previous post, we implemented and configured the messaging infrastructure thereby now have a fully working example from request to response and from the controller, the message handler to a repository and all the way back.

The last thing from our list is the documentation of the API.

Author
Michael Zangerle
Date
March 5, 2021
Reading time
8 Minutes

API documentation

We want to provide a documentation of the API for everyone using or depending on it. But we also want to get the most out of our current structure and not type everything by hand. Luckily there exists a Symfony Bundle called NelmioApiDocBundle which does just that. It can use classes to generate a schema for requests and responses and provides Swagger documentation.

Enabling it

Configuring the bundle and adding the necessary routes is quite easy and looks in case of our example application like this:

nelmio_api_doc: documentation: info: title: My App description: This is an awesome app! version: 1.0.0 areas: path_patterns: - ^/api(?!/doc)

Now the only thing we need to do is to add the necessary annotations to the controller actions and let the bundle do it's magic.

Documenting endpoints

First let's have a look at our CustomerController to get started. There we find the following annotations on the actions methods:

Tag is used to group related APIs e.g. all customer-related APIs should be grouped together in one place.

Response and Model enable us to let the user of the API know what values and types can be expected in the response. We can also add multiple responses with different status codes for the error cases.

RequestBody in combination with Model allows us to define how the request body should look like and if it's required at all, partially or completely. The single properties are then retrieved from the class itself and depending on their type, validation and whether they are nullable or not they will show up respectively in the docs.

<?php namespace App\Customer\Controller; // .. final class CustomerController extends AbstractController { // ... /** * @OA\Tag(name="Customer") * @OA\RequestBody(@Model(type=ChangeCustomerTypeCommand::class), required=true) * @OA\Response( * @Model(type=CustomerResponse::class), * response=Response::HTTP_OK, * description="Successful change of customer type." * ) * @Route("/{id}/changeType", methods={"POST"}, requirements={"id"="\d+"}) */ public function changeTypeAction(ChangeCustomerTypeCommand $command): Response { // .. } }

Currently we only specified what models should be used to generate the documentation. But we might also want to define which values are valid for those models. For that the bundle makes use of Symfony Validator annotations and processes them as well. A good example is the ChangeCustomerTypeCommand where the possible type values will be validated against the defined ones and this information will also be shown in the generated API documentation.

/** * @Assert\NotNull(message="Type should not be null.") * @Assert\Choice( * choices=App\Entity\Customer::TYPES, * message="Customer type should match one of existing types." * ) */ private string $type;

That is all we need to create a documentation for our API. As it's a bit time consuming to find the correct way to annotate different cases let's have a look at some more complex examples.

Query parameters and models

Our customers API provides a get request for the collection of all customers and we also have some filtering query parameters (first name, last name) which can be applied. To show this information also in the documentation we have to add the parameter annotation and define which object should be used for the generation.

/** * @OA\Tag(name="Customer") * @OA\Parameter(name="query", in="query", explode=true, @OA\Schema(ref=@Model(type=GetCustomersQuery::class))) * @OA\Response( * @Model(type=CustomerListResponse::class), * response=Response::HTTP_OK, * description="Successful fetch of customers." * ) * @Route("", methods={"GET"}, requirements={"id"="\d+"}) */ public function cgetAction(GetCustomersQuery $query): Response { // ... }

Returning a collection of models

This API will also return a collection of customers. To create our desired response structure we create a list response class which holds all the customers in an array. As we don’t have generics we have to tell the Bundle of which type the values of the array are and this can be done in the response class like this:

/** * @OA\Property(type="array", @OA\Items(ref=@Model(type=CustomerResponse::class))) * * @return array<CustomerResponse> */ public function getCustomers(): array { return $this->customers; }

Summary

Now we really have a fully working and documented example which should cover most of the common cases to get you started. We used only one Bundle and stuck to Symfony Components as much as possible.  If you are already working with PHP 8, you gain a big additional benefit by using the native attributes.

Using an IDE like PHPStorm should make it quite easy to follow the flow in your code. Of course with events and async processing you might add some complexity but every piece you add should be equally small as every command and query handler so that its purpose is easy to grasp.

As mentioned at the beginning the approach is pragmatic but not without reason. I hope it helps you to get started with less entangled and better code and architecture.

Thanks

Thanks to my colleagues @arjanfrans, @mburtscher and @rothdave for helping me with this series and for a lot of valuable input.

More of that?

Parsing_csv file the right way in php
Dev Diary
Parsing csv files the right way in php 8
April 7, 2021 | 2 Min.
Pragmatic architecture 4_CQRS-messaging_B
Dev Diary
Pragmatic architecture 4: CQRS and Messaging
March 4, 2021 | 4 Min.

Contact form

*Required field
*Required field
*Required field
*Required field
We protect your privacy

We keep your personal data safe and do not share it with third parties. You can find out more about this in our privacy policy.