How to Create a Simple REST API with Laravel Lumen
Tags: laravel, api, webdev, tutorial
Published: 2023-03-15 14:51:49

I recently had my first experience with Laravel Lumen and I decided to share my knowledge. I created an Articles REST API and I'll show how I did this step-by-step.
 
Before we move on to create a simple REST API with Laravel Lumen, I want to first describe Laravel Lumen;

Laravel Lumen is a lightweight and micro-framework designed for building fast and efficient API applications and microservices using the PHP programming language. It is a fully featured, blazing fast micro-framework that is built on top of Laravel's components and provides a simple and elegant syntax for building web applications and RESTful APIs.
It is a great option for creating small, high-performance microservices that require quick response times compared to Laravel.


To create a simple Lumen REST API:
First create your lumen project, this is very similar to creating a laravel app. Here I named my application rest-api:

`composer create-project laravel/lumen rest-api`

Next cd into your created application and run

`composer install`

By default, Lumen doesn't allow you to generate application keys, to generate app keys, install Lumen App Key Generator.
Simply install [Lumen Key Generator](https://github.com/maxsky/Lumen-AppKey-Generator) and then you can set an application key using the normal `php artisan key:generate` command.

`composer require maxsky/lumen-app-key-generator`

N.B: You can find the list of all artisan commands available using `php artisan`

Once this is done, you can serve up the application using the following command:

`php -S localhost:8000 -t public`

Ensure you create a database and add its credentials to the .env file in your project.

Within your routes/web.php file, you can define your API endpoints using Lumen's routing system.

Next you can move on to create controllers to handle the logic for each endpoint. 
The command is similar to the command used in Laravel to create controllers:

`php artisan make:controller MyController`

For my rest applications, I like defining my success and error response functions within the Controller.php and simply use it within the created controllers, I find this really effective and it makes my code cleaner.

So within my Controller.php I add the following lines
```
 protected function errorResponse($message, $errors=null, $code=422) {
        if($message == null && is_string($errors))
            $message = $errors;
        return response()->json([
            'errors' => $errors,
            'data' => null,
            'message' => $message,
            'status' => 'error'
            ], $code);
    }

    protected function successResponse($message, $data=null, $code=200) {
        return response()->json([
            'errors' => null,
            'data' => $data,
            'message' => $message,
            'status' => 'success'
        ], $code);
    }
```
such that the controller.php file now looks like this:

```
<?php

namespace App\Http\Controllers;

use Laravel\Lumen\Routing\Controller as BaseController;

class Controller extends BaseController
{
    protected function errorResponse($message, $errors=null, $code=422) {
        if($message == null && is_string($errors))
            $message = $errors;
        return response()->json([
            'errors' => $errors,
            'data' => null,
            'message' => $message,
            'status' => 'error'
            ], $code);
    }

    protected function successResponse($message, $data=null, $code=200) {
        return response()->json([
            'errors' => null,
            'data' => $data,
            'message' => $message,
            'status' => 'success'
        ], $code);
    }
}
```
Once we have that sorted out, we create the needed migration by using the `php artisan make:migration migrationName` command.

For this project, I created 3 migrations:

- Articles
- Tags
- Comments

So, each of them are created using the following commands, following this order due to the relations that we intend to create between them:

`php artisan make:migration create_tags_table`
`php artisan make:migration create_articles_table`
`php artisan make:migration create_comments_table`

Within database/migrations folder, we find all our created migrations, we start editing our migration files to look like this:

1. The create tags migration
```
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tags');
    }
};
```

2. The create articles migration

```
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title');
            $table->text('cover');
            $table->text('full_text');
            $table->bigInteger('likes_counter');
            $table->bigInteger('views_counter');
            $table->unsignedBigInteger('tags_id');
            $table->foreign('tags_id')->references('id')->on('tags')->onDelete('cascade');
            $table->timestamps();
        });
    }

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

```

3. The create comments table:

```
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('articles_id');
            $table->foreign('articles_id')->references('id')->on('articles')->onDelete('cascade');
            $table->string('subject');
            $table->longText('body');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('comments');
    }
};
```
Next we move on to create our models:
Unlike Laravel, with Lumen you cannot create models using the php artisan method, so we navigate to app/Models folder and create the following models mapping our migrations:

- Articles
- Comments
- Tags

1. The Articles.php file now looks like this:

```
<?php

namespace App\Models;

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

class Articles extends Model
{
    use HasFactory;

    protected $guarded = [];

    protected $hidden = [
        'likes_counter',
        'views_counter',
        'tags_id',
        'full_text',
        'created_at', 
        'updated_at'
    ];

    public function tags()
    {
        return $this->belongsTo(Tags::class);
    }

    public function comments()
    {
        return $this->hasMany(Comments::class);
    }

}
```

2. The Comments.php file now looks like this:
```
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comments extends Model
{
    protected $guarded = [];

    public function articles()
    {
        return $this->belongsTo(Articles::class);
    }

    public function rules()
    {
        return [
            'body' => 'required|string',
            'subject' => 'required|string'
        ];
    }
}
```

3. The Tags.php file now looks like this:
```
<?php

namespace App\Models;

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

class Tags extends Model
{
    use HasFactory;
    protected $guarded = [];

    protected $hidden = [
        'created_at', 
        'updated_at'
    ];

    public function articles()
    {
        return $this->hasMany(Articles::class);
    }
}
```

Now that we have created our models and the relationships between them, we can then move on to create our controller and the functions within them. We create an ArticlesController.php file within the Controller folder.

We add the different functions to view articles, like article, view single article and also add comment to article. The ArticlesController.php file now looks like this:

```
<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Articles;
use App\Models\Comments;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;

class ArticlesController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Return all articles
     */
    public function index()
    {
        $articles = Articles::orderBy('id', 'desc')
            ->with('tags')
            ->paginate(10);
        foreach ($articles as $article) {
            $article->short_description = Str::limit($article->full_text, 100);
        }
        return $this->successResponse('All Articles', $articles, 200);   
    }

    /**
     * @param $id
     * Like article
     */
    public function likeArticle($id)
    {
        try {
            $article = Articles::findOrFail($id);
            $article->likes_counter = (int)($article->likes_counter) + 1;
            $article->save();
            return $this->successResponse('TotalLikes', $article->likes_counter, 200);
        } catch (\Throwable $th) {
            return $this->errorResponse('An error occurred', $th->getMessage(), 400);
        }
        
    }

    /**
     * @param $id
     * View article
     */
    public function viewArticle($id)
    {
        try {

            $cacheKey = "article_{$id}_views";
            $cacheDuration = 5; // in seconds

            // Check if the view count is already in the cache
            $viewCount = Cache::get($cacheKey, 0);

            // If the view count is not in the cache, fetch it from the database
            if ($viewCount === 0) {
                $article = Articles::findOrFail($id);
                $viewCount = $article->views_counter;
            }


            $viewCount++;
            Cache::put($cacheKey, $viewCount, $cacheDuration);

            // Check if it's time to write the view count to the database
            if (time() % 5 === 0) {
                $article = Articles::findOrFail($id);
                $article->views_counter = $viewCount;
                $article->save();
            }
            return $this->successResponse('TotalViews', $article->views_counter, 200);  
        } catch (\Throwable $th) {
            return $this->errorResponse('An error occurred', $th->getMessage(), 400);
        }

    }
    
    /**
     * @param $id
     * Comment on article
     */
    public function commentOnArticle(Request $request, $id)
    {
        DB::beginTransaction();
        try {
            $comment = new Comments();
            $validation = Validator::make($request->all(), $comment->rules());
            if($validation->fails()) {
                return $this->errorResponse('Validation error', $validation->errors(), 400);
            }
            $articleId = Articles::lockForUpdate()->findOrFail($id);
            $comment->body = $request->body;
            $comment->subject = $request->subject;
            $comment->articles_id = $articleId->id;
            $comment->save();
            DB::commit();
            return $this->successResponse('Your comment has been successfully sent', $comment, 200);
        } catch (\Exception $e) {
            DB::rollback();
            return $this->errorResponse('Your comment was not saved', $e->getMessage(), 400);
        }
    }

    /**
     * @param $id
     * View single article
     */
    public function viewSingleArticle($id)
    {
        try {
            $article = Articles::findOrFail($id);
            $article->with('tags', 'comments');
            $article->makeVisible(['likes_counter', 'views_counter', 'tags_id', 'full_text']);
            return $this->successResponse('Single Article', $article, 200);
        } catch (\Throwable $th) {
            return $this->errorResponse('An error occurred error', $th->getMessage(), 400);
        }
    }
}

```

Within the routes/web.php file, we add the following lines:
```
$router->group(['prefix' => 'api'], function () use ($router) {
    $router->get('articles', ['uses' => 'ArticlesController@index']);
    $router->get('articles/{id}', ['uses' => 'ArticlesController@viewSingleArticle']);
    $router->post('articles/{id}/like', ['uses' => 'ArticlesController@likeArticle']);
    $router->post('articles/{id}/view', ['uses' => 'ArticlesController@viewArticle']);
    $router->post('articles/{id}/comment', ['uses' => 'ArticlesController@commentOnArticle']);

});
```

We can move on to test the endpoints using insomnia or postman.

The source code for this project can be found [here](https://github.com/Osalumense/REST-API)

Thanks for reading.
Don't forget to like, comment and save this article for later use.