Laravel API documentation with Swagger

314
api-documentation-with-swagger

Today, we will learn about creating beautiful API documentation with Swagger in Laravel. API documentation is very important when working in a team. It helps to determine the endpoints and parameters required for any request.

In this article, we will use a package known as DarkaOnLine/L5-Swagger in our Laravel application. This package is popular and specifically built to use on Laravel applications.

Before we head towards writing API documentation, we must install this package by running the below command.

composer require "darkaonline/l5-swagger"

This will install the package. Then we can publish the configuration and view files with the below command.

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

Now, let’s set up our project to create APIs with CRUD functionality for Article. Run the below command to create migration for articles table.

php artisan make:migration create_articles_table

We will add some columns on the table. So that our final migrations for articles table looks like below.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content')->nullable();
            $table->enum('status', ['Published', 'Draft'])->default('Draft');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('articles');
    }
};

Run the migration to create table.

php artisan migrate

Let’s create factory to seed the articles table in the database.

php artisan make:factory Article

Open up the newly created ArticleFactory.php and populate the columns using faker. Faker is a library that generates fake data for us. If you want to know more about faker, you can read this article.

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
 */
class ArticleFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'title'   => $this->faker->realText($maxNbChars = 50, $indexSize = 2),
            'content' => $this->faker->paragraph(10, false),
            'status'  => $this->faker->randomElement(['Published', 'Draft'])
        ];
    }
}

Add this seeder file in the /seeders/DatabaseSeeder.php file.

<?php

//database/seeders/DatabaseSeeder.php

................

public function run()
{
    //seed users table
    \App\Models\User::factory(2)->create();
    //seed articles table
    \App\Models\Article::factory(50)->create();
}

Run the seed. This command will populate our articles table with specified(50) new records.

php artisan db:seed

Now, let’s create a model for the articles table. Run the command.

php artisan make:model Article

We will change few settings in our model. The Article.php file looks as below.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'content', 'status'];
}

Now, after all our setups are done, we will move into creating the API documentation. First of all, we need to give a description of the API. To do so, we need to modify our Controller class. We will add a few descriptions above the class inside the comment section. Please note that the code inside comment section with proper format will be used as API documentation.

<?php

//app/Http/Controllers/Controller.php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

/**
 * @OA\Info(
 *     version="1.0.0",
 *     title="Kodementor Api Documentation",
 *     description="Kodementor Api Documentation",
 *     @OA\Contact(
 *         name="Vijay Rana",
 *         email="[email protected]"
 *     ),
 *     @OA\License(
 *         name="Apache 2.0",
 *         url="http://www.apache.org/licenses/LICENSE-2.0.html"
 *     )
 * ),
 * @OA\Server(
 *     url="/api/v1",
 * ),
 */
class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

This document inside will generate the following description of the API.

Please note that we will use /api/v1 route for the API. so, we will need to add v1 to the API route in RouteServiceProvider.

<?php

//app/Providers/RouteServiceProvider.php

..........
public function boot()
{
    $this->configureRateLimiting();

    //please note the /v1 route prefix
    $this->routes(function () {
        Route::prefix('api/v1')
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));

        Route::middleware('web')
            ->namespace($this->namespace)
            ->group(base_path('routes/web.php'));
    });
}

Now, here comes the final part of writing documentation for the API. We store all the API controller inside a separate api/ directory. We will create API documentation for the index, store, edit, update and delete methods. I have added the API documentation above each method. Thus, our final ArticleController.php file looks like below.

<?php

namespace App\Http\Controllers\Api;

use Exception;
use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Api\BaseController;

class ArticleController extends Controller
{
    /**
     * @OA\Get(
     *    path="/articles",
     *    operationId="index",
     *    tags={"Articles"},
     *    summary="Get list of articles",
     *    description="Get list of articles",
     *    @OA\Parameter(name="limit", in="query", description="limit", required=false,
     *        @OA\Schema(type="integer")
     *    ),
     *    @OA\Parameter(name="page", in="query", description="the page number", required=false,
     *        @OA\Schema(type="integer")
     *    ),
     *    @OA\Parameter(name="order", in="query", description="order  accepts 'asc' or 'desc'", required=false,
     *        @OA\Schema(type="string")
     *    ),
     *     @OA\Response(
     *          response=200, description="Success",
     *          @OA\JsonContent(
     *             @OA\Property(property="status", type="integer", example="200"),
     *             @OA\Property(property="data",type="object")
     *          )
     *       )
     *  )
     */
    public function index(Request $request)
    {
        try {
            $limit = $request->limit ?: 15;
            $order = $request->order == 'asc' ? 'asc' : 'desc';

            $articles = Article::orderBy('updated_at', $order)
                ->select('id', 'title', 'content', 'status')
                ->where('status', 'Published')
                ->paginate($limit);

            return response()->json(['status' => 200, 'data' => $articles]);
        } catch (Exception $e) {
            return response()->json(['status' => 400, 'message' => $e->getMessage()]);
        }
    }

    /**
     * @OA\Post(
     *      path="/articles",
     *      operationId="store",
     *      tags={"Articles"},
     *      summary="Store article in DB",
     *      description="Store article in DB",
     *      @OA\RequestBody(
     *         required=true,
     *         @OA\JsonContent(
     *            required={"title", "content", "status"},
     *            @OA\Property(property="title", type="string", format="string", example="Test Article Title"),
     *            @OA\Property(property="content", type="string", format="string", example="This is a description for kodementor"),
     *            @OA\Property(property="status", type="string", format="string", example="Published"),
     *         ),
     *      ),
     *     @OA\Response(
     *          response=200, description="Success",
     *          @OA\JsonContent(
     *             @OA\Property(property="status", type="integer", example=""),
     *             @OA\Property(property="data",type="object")
     *          )
     *       )
     *  )
     */
    public function store(Request $request)
    {
        try {
            DB::beginTransaction();

            $article = Article::create($request->only('title', 'content', 'status'));
            DB::commit();

            return response()->json(['status' => 201, 'data' => $article]);
        } catch (Exception $e) {
            DB::rollBack();
            return response()->json(['status' => 400, 'message' => $e->getMessage()]);
        }
    }

    /**
     * @OA\Get(
     *    path="/articles/{id}",
     *    operationId="show",
     *    tags={"Articles"},
     *    summary="Get Article Detail",
     *    description="Get Article Detail",
     *    @OA\Parameter(name="id", in="path", description="Id of Article", required=true,
     *        @OA\Schema(type="integer")
     *    ),
     *     @OA\Response(
     *          response=200,
     *          description="Success",
     *          @OA\JsonContent(
     *          @OA\Property(property="status_code", type="integer", example="200"),
     *          @OA\Property(property="data",type="object")
     *           ),
     *        )
     *       )
     *  )
     */
    public function show(Article $article)
    {
        try {
            return response()->json(['status' => 200, 'data' => $article]);
        } catch (Exception $e) {
            return response()->json(['status' => 400, 'message' => $e->getMessage()]);
        }
    }

    /**
     * @OA\Put(
     *     path="/articles/{id}",
     *     operationId="update",
     *     tags={"Articles"},
     *     summary="Update article in DB",
     *     description="Update article in DB",
     *     @OA\Parameter(name="id", in="path", description="Id of Article", required=true,
     *         @OA\Schema(type="integer")
     *     ),
     *     @OA\RequestBody(
     *        required=true,
     *        @OA\JsonContent(
     *           required={"title", "content", "status"},
     *           @OA\Property(property="title", type="string", format="string", example="Test Article Title"),
     *           @OA\Property(property="content", type="string", format="string", example="This is a description for kodementor"),
     *           @OA\Property(property="status", type="string", format="string", example="Published"),
     *        ),
     *     ),
     *     @OA\Response(
     *          response=200, description="Success",
     *          @OA\JsonContent(
     *             @OA\Property(property="status_code", type="integer", example="200"),
     *             @OA\Property(property="data",type="object")
     *          )
     *       )
     *  )
     */
    public function update(Request $request, $id)
    {
        try {
            DB::beginTransaction();

            $article = Article::updateOrCreate(['id' => $id], $request->only('title', 'content', 'status'));
            DB::commit();
            return response()->json(['status' => 200, 'data' => $article]);
        } catch (Exception $e) {
            DB::rollback();
            return response()->json(['status' => 400, 'message' => $e->getMessage()]);
        }
    }

    /**
     * @OA\Delete(
     *    path="/articles/{id}",
     *    operationId="destroy",
     *    tags={"Articles"},
     *    summary="Delete Article",
     *    description="Delete Article",
     *    @OA\Parameter(name="id", in="path", description="Id of Article", required=true,
     *        @OA\Schema(type="integer")
     *    ),
     *    @OA\Response(
     *         response=200,
     *         description="Success",
     *         @OA\JsonContent(
     *         @OA\Property(property="status_code", type="integer", example="200"),
     *         @OA\Property(property="data",type="object")
     *          ),
     *       )
     *      )
     *  )
     */
    public function destroy($id)
    {
        try {
            Article::find($id)->delete();

            return response()->json(['status' => 200, 'data' => []]);
        } catch (Exception $e) {
            return response()->json(['status' => 400, 'message' => $e->getMessage()]);
        }
    }
}

In the above code, we will accept multiple parameters for the index method. This is because the ordering and pagination will be handled by the end user. Thus, we will fetch the record and respond accordingly.

API list

While executing the API, we can get the below response. You can test it with other APIs as well.

API response for displaying list of articles

This is all about swagger implementation in Laravel for API documentation. Furthermore, we can also format the response sent to the API with API Resource. We will come up with the documentation as well.

Please drop your thoughts in the comment section below.

Read More Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

This site uses Akismet to reduce spam. Learn how your comment data is processed.