As a final lesson in our course, we must provide documentation for someone who wants to consume our API. As the client stated, "Creating docs is big plus".
In Laravel, there's a package to generate the HTML page with the documentation from our configuration and DocBlock comments in Controller files.
It's called Scribe.
To install Scribe, you will need to require it using Composer:
composer require --dev knuckleswtf/scribe
Next, publish the config file:
php artisan vendor:publish --tag=scribe-config
This will publish a scribe.php
file in your config directory, where we will later add our configuration to ensure Scribe works as we want.
And you can already generate the documentation! Run this:
php artisan scribe:generate
Then we visit the page /docs/index.html
(or just /docs
if your web server is configured for it) and already see our endpoints listed on the left!
The screenshots from the final docs are at the end of this lesson.
Notice: this generated list reminded me that we didn't remove the default /api/user
Route generated by Laravel. We don't need it in this project. Let's remove it.
Now, of course, what Scribe generated automatically, is missing a lot of essential details. We need to:
In the main config/scribe.php
, we can change many values. The file itself is pretty huge, and I will display only a few options here below:
config/scribe.php:
return [ 'title' => null, 'description' => '', 'base_url' => null, 'routes' => [ [ 'match' => [ 'prefixes' => ['api/*'], 'domains' => ['*'], 'versions' => ['v1'], ], // ... ], ], 'auth' => [ 'enabled' => false, 'default' => false, 'in' => 'bearer', 'name' => 'key', 'use_value' => env('SCRIBE_AUTH_KEY'), 'placeholder' => '{YOUR_AUTH_KEY}', ], 'intro_text' => <<<INTROThis documentation aims to provide all the information you need to work with our API. <aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>INTRO , // ...];
The most important ones are probably these:
title
: will be displayed at the top of the pageintro_text
- will be displayed in the Introduction sectiondescription
: displayed in the Introduction, Postman Collection, and OpenAPI sectionsbase_url
- you should fill it with your production URLroutes
- which routes to generate the docs for, which ones to include/excludeauth
- configures the authentication for your APIFeel free to play around with these values and other config settings.
So, we added the general information. We need to add DocBlock comments to our Controllers so that Scribe would generate the correct documentation for them.
Example:
app/Http/Controllers/Api/V1/TravelController.php:
/** * @group Public endpoints */class TravelController extends Controller{ /** * GET Travels * * Returns paginated list of travels. * * @queryParam page integer Page number. Example: 1 * * @response {"data":[{"id":"9958e389-5edf-48eb-8ecd-e058985cf3ce","name":"First travel", ...}} */ public function index() { $travels = Travel::query() ->where('is_public', true) ->paginate(config('app.paginationPerPage.travels')); return TravelResource::collection($travels); }}
What parameters are we adding here?
@group
, which will generate the sub-menu parent item on the leftYou can generate that response in Postman and then just copy-paste it here, pre-formatting it into one line with some online JSON converter.
For the public TourController
, we need to also describe the optional GET parameters with this syntax:
app/Http/Controllers/Api/V1/TourController.php:
/** * @group Public endpoints */class TourController extends Controller{ /** * GET Travel Tours * * Returns paginated list of tours by travel slug. * * @urlParam travel_slug string Travel slug. Example: "first-travel" * * @bodyParam priceFrom number. Example: "123.45" * @bodyParam priceTo number. Example: "234.56" * @bodyParam dateFrom date. Example: "2023-06-01" * @bodyParam dateTo date. Example: "2023-07-01" * @bodyParam sortBy string. Example: "price" * @bodyParam sortOrder string. Example: "asc" or "desc" * * @response {"data":[{"id":"9958e389-5edf-48eb-8ecd-e058985cf3ce","name":"Tour on Sunday","starting_date":"2023-06-11","ending_date":"2023-06-16", ...} * */ public function index(Travel $travel, ToursListRequest $request) { $tours = $travel->tours() // ->when(...) ->orderBy('starting_date') ->paginate(); return TourResource::collection($tours); }}
Here's how it looks after we re-generate the docs:
Next, for LoginController, we add these parameters:
app/Http/Controllers/Api/V1/Auth/LoginController.php:
/** * @group Auth endpoints */class LoginController extends Controller{ /** * POST Login * * Login with the existing user. * * @response {"access_token":"1|a9ZcYzIrLURVGx6Xe41HKj1CrNsxRxe4pLA2oISo"} * @response 422 {"error": "The provided credentials are incorrect."} */ public function __invoke(LoginRequest $request) { // ... }
As you can see, you should provide all possible return responses: both successful and unsuccessful.
Finally, for Admin/TravelController
and Admin/TourController
, we add another endpoint, @group
, and add the @authenticated
line, so Scribe would show that these need authentication.
app/Http/Controllers/Api/V1/Admin/TravelController.php:
/** * @group Admin endpoints */class TravelController extends Controller{ /** * POST Travel * * Creates a new Travel record. * * @authenticated * * @response {"data":{"id":"996a36ca-2693-4901-9c55-7136e68d81d5","name":"My new travel 234","slug":"my-new-travel-234", ...} * @response 422 {"message":"The name has already been taken.","errors":{"name":["The name has already been taken."]}} */ public function store(TravelRequest $request) { $travel = Travel::create($request->validated()); return new TravelResource($travel); } /** * PUT Travel * * Updates new Travel record. * * @authenticated * * @response {"data":{"id":"996a36ca-2693-4901-9c55-7136e68d81d5","name":"My new travel 234", ...} * @response 422 {"message":"The name has already been taken.","errors":{"name":["The name has already been taken."]}} */ public function update(Travel $travel, TravelRequest $request) { $travel->update($request->validated()); return new TravelResource($travel); }}
And that's it as a bare minimum, we've generated our API documentation!
A few screenshots:
Notice: Keep in mind that I didn't try to be accurate with all possible parameters and their values. My goal here was to show you the process with the main configuration points. For all the details of syntax values, please read the official Scribe documentation.