Back to Course |
Laravel Travel Agency API From Scratch

API Docs with Scribe

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.


Installation

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:

  • Provide general description info of the project
  • Group those Endpoints into public/admin sub-menus
  • Provide information about Auth
  • Provide information about parameters and example responses

Global Scribe Configuration

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' => <<<INTRO
This 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 page
  • intro_text - will be displayed in the Introduction section
  • description: displayed in the Introduction, Postman Collection, and OpenAPI sections
  • base_url - you should fill it with your production URL
  • routes - which routes to generate the docs for, which ones to include/exclude
  • auth - configures the authentication for your API

Feel free to play around with these values and other config settings.


Documenting API Endpoints

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?

  1. On top of the whole Controller: we add a @group, which will generate the sub-menu parent item on the left
  2. Before every method, we add the "Title", the "Description", a list of all parameters with their types, and a typical response

You 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.


GitHub commit for this lesson: