API documentation becomes very necessary when you split the team into Backend and Frontend. And even more when you divide your monorepo into parts or even microservices.

I will show you how easily create API documentation for your Laravel API using swagger.

Let’s start. I prefer using this package. This package is a wrapper of Swagger-php and swagger-ui adapted to work with Laravel.

composer require "darkaonline/l5-swagger"

Then publish config and all view files:

php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"

Next, open a config/l5-swagger.php file. Let’s walk through essential keys:

  • routes.api — This is an URL for accessing documentation UI. Your frontend team will be using it to access documentation. By default, it is api/documentation . I prefer changing it to something smaller like api/docs
  • generate_always — I prefer disabling it as it will generate docs on the fly. Not useful with big API. You can always manually run php artisan l5-swagger:generate

Those are the most important to start. Now if you try to create docs using this command

php artisan l5-swagger:generate

It will return an error

Required @OA\Info() not found

That means that you have to create that notation first. So let’s add it. I prefer creating Abstract controller for an API, but you can add this to app/Http/Controllers/Controller.php

/**
 * @OA\Info(
 *    title="Your super  ApplicationAPI",
 *    version="1.0.0",
 * )
 */
class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

Next, we need to add docs for at least one route. Let’s add it for app/Http/Controllers/Auth/LoginController.php:

/**
 * @OA\Post(
 * path="/login",
 * summary="Sign in",
 * description="Login by email, password",
 * operationId="authLogin",
 * tags={"auth"},
 * @OA\RequestBody(
 *    required=true,
 *    description="Pass user credentials",
 *    @OA\JsonContent(
 *       required={"email","password"},
 *       @OA\Property(property="email", type="string", format="email", example="user1@mail.com"),
 *       @OA\Property(property="password", type="string", format="password", example="PassWord12345"),
 *       @OA\Property(property="persistent", type="boolean", example="true"),
 *    ),
 * ),
 * @OA\Response(
 *    response=422,
 *    description="Wrong credentials response",
 *    @OA\JsonContent(
 *       @OA\Property(property="message", type="string", example="Sorry, wrong email address or password. Please try again")
 *        )
 *     )
 * )
 */

Now, you are ready to go. Run php artisan l5-swagger:generate and go to the URL that you provided in your config. In my case, it will be http://127.0.0.1:8000/api/docs.

You will see something like that:

Swagger UI layout

Swagger UI layout


Swagger UI main page

Now let’s dig into annotations. I will try to explain how to use them:

  • @OA — means Open API annotation. You can read more here
  • @OA\Post — means POST request. There are GET, POST, DELETE, etc.
  • Path — it’s an URL
  • Tags — it will group your API by sections.
  • @OA\RequestBody — it’s obvious from the name. It should have JsonContent annotation inside with Property annotations(i.e., field descriptions).
  • @OA\Response — you can have as many responses as you want. You should provide all possible success and error responses here.

Let’s add a 200 response code:

*   @OA\Response(
*     response=200,
*     description="Success",
*     @OA\JsonContent(
*        @OA\Property(property="user", type="object", ref="#/components/schemas/User"),
*     )
*  ),

@OA\Property annotation has a property key(field name) and type. Type can have different values: string, object, integer, array, boolean, etc.

In this response, I used the type object. You can pass a reference to that object. Let’s create a User object. I prefer adding that in Model class.

/**
 *
 * @OA\Schema(
 * required={"password"},
 * @OA\Xml(name="User"),
 * @OA\Property(property="id", type="integer", readOnly="true", example="1"),
 * @OA\Property(property="role", type="string", readOnly="true", description="User role"),
 * @OA\Property(property="email", type="string", readOnly="true", format="email", description="User unique email address", example="user@gmail.com"),
 * @OA\Property(property="email_verified_at", type="string", readOnly="true", format="date-time", description="Datetime marker of verification status", example="2019-02-25 12:59:20"),
 * @OA\Property(property="first_name", type="string", maxLength=32, example="John"),
 * @OA\Property(property="last_name", type="string", maxLength=32, example="Doe"),
 * @OA\Property(property="created_at", ref="#/components/schemas/BaseModel/properties/created_at"),
 * @OA\Property(property="updated_at", ref="#/components/schemas/BaseModel/properties/updated_at"),
 * @OA\Property(property="deleted_at", ref="#/components/schemas/BaseModel/properties/deleted_at")
 * )
 *
 * Class User
 *
 */

Take a look at this notation @OA\Xml(name=” User”). This name will be used in a ref key of the @OA\Property

As you can see I used #/components/schemas/BaseModel/properties/* for created_at, updated_at and deleted_at. I prefer creating abstract BaseModel with repeatable annotations, but you can put them anywhere you want. It is just my preferable way of structuring it.

/**
 * @OA\Schema(
 * @OA\Property(property="created_at", type="string", format="date-time", description="Initial creation timestamp", readOnly="true"),
 * @OA\Property(property="updated_at", type="string", format="date-time", description="Last update timestamp", readOnly="true"),
 * @OA\Property(property="deleted_at", type="string", format="date-time", description="Soft delete timestamp", readOnly="true"),
 * )
 * Class BaseModel
 *
 * @package App\Models
 */
abstract class BaseModel extends Model {}

Another tip for authorizing some URLs with JWT. You can add a security parameter this way:

/**
 * @OA\Post(
 * path="/v1/logout",
 * summary="Logout",
 * description="Logout user and invalidate token",
 * operationId="authLogout",
 * tags={"auth"},
 * security={ {"bearer": {} }},
 * @OA\Response(
 *    response=200,
 *    description="Success"
 *     ),
 * @OA\Response(
 *    response=401,
 *    description="Returns when user is not authenticated",
 *    @OA\JsonContent(
 *       @OA\Property(property="message", type="string", example="Not authorized"),
 *    )
 * )
 * )
 */

Now you will see a locker icon near the route. When you click on that, you will be able to add Bearer token

Bearer token icon

Bearer token icon

Bearer token value

Bearer token value


A few more examples of different @OA\Property types:

  1. You can use predefined format=” email” and even regexp pattern.


@OA\Property(property="email", type="string", pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$", format="email", example="user2@gmail.com"),

2. You can use type=” array” and collectionFormat=” multi” to describe an array of validation errors. But you have to define @OA\Items annotation

* @OA\Response(
*     response=422,
*     description="Validation error",
*     @OA\JsonContent(
*        @OA\Property(property="message", type="string", example="The given data was invalid."),
*        @OA\Property(
*           property="errors",
*           type="object",
*           @OA\Property(
*              property="email",
*              type="array",
*              collectionFormat="multi",
*              @OA\Items(
*                 type="string",
*                 example={"The email field is required.","The email must be a valid email address."},
*              )
*           )
*        )
*     )
*  )

3. You can provide a binary image as an example if your backend supports that

@OA\Property(property="picture", type="string", format="base64", example="data:image/jpeg;base64, yourSuperLongStringBinary"),

or like binary

@OA\Property(property="file", type="string", format="binary"),

4. When you need to describe an array of objects you can use type=” array” and pass object via @OA\Items

@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/City"))

And a few more advanced examples with parameters and combined/complex schemas

  1. You can create combined schemas using allOf,anyOf, etc. profileGet schema will contain User object, an array of OrderCategory objects, etc.
/**
 * @OA\Schema(
 *     schema="profileGet",
 * allOf={
 *    @OA\Schema(ref="#/components/schemas/User"),
 *    @OA\Schema(
 *       @OA\Property(property="categories", type="array", @OA\Items(ref="#/components/schemas/OrderCategory")),
 *    ),
 *    @OA\Schema(
 *       @OA\Property(property="locations", type="array", @OA\Items(ref="#/components/schemas/stateCounties")),
 *    ),
 *    @OA\Schema(
 *       @OA\Property(property="avatar", type="object", ref="#/components/schemas/File"),
 *    ),
 *    @OA\Schema(
 *       @OA\Property(property="address", type="object", ref="#/components/schemas/AddressCoordinates"),
 *    )
 * }
 * )
 *
 * @OA\Get(
 * path="/v1/profile",
 * summary="Retrieve profile information",
 * description="Get profile short information",
 * operationId="profileShow",
 * tags={"profile"},
 * security={ {"bearer": {} }},
 * @OA\Response(
 *    response=200,
 *    description="Success",
 *    @OA\JsonContent(
 *       @OA\Property(property="data", type="object", ref="#/components/schemas/profileGet")
 *        )
 *     ),
 * @OA\Response(
 *    response=401,
 *    description="User should be authorized to get profile information",
 *    @OA\JsonContent(
 *       @OA\Property(property="message", type="string", example="Not authorized"),
 *    )
 * )
 * )
 */

2. Whenever you need to describe parameter in URL (e.g. /v1/geo/cities/{cityId}/zip_codes )you can use @OA\Parameter.

/**
 * @OA\Get(
 * path="/v1/geo/cities/{cityId}/zip_codes",
 * summary="List of zip codes by city",
 * description="Get list of zip codes by city",
 * operationId="geoZipCodes",
 * tags={"geo"},
 * security={ {"bearer": {} }},
 * @OA\Parameter(
 *    description="ID of city",
 *    in="path",
 *    name="cityId",
 *    required=true,
 *    example="1",
 *    @OA\Schema(
 *       type="integer",
 *       format="int64"
 *    )
 * )
 * )
 */

There are many other cool features and options that you can use to create informative API documentation. I tried to cover the most important things that took a lot of my time when I was creating swagger documentation. Feel free to share your snippets!


Original post: https://ivankolodiy.medium.com/how-to-write-swagger-documentation-for-laravel-api-tips-examples-5510fb392a94

Materials:

My socials:

https://ivankolodiy.medium.com/

https://www.facebook.com/kolodii.ivan

https://www.instagram.com/kolodiy.ivan

https://twitter.com/PoTHuY_JoHN