In todays's tutorial, we are going to build a product reviews API using Laravel. In this API,Users will be able to add, view, update and delete products and also they will also be able to rate and review a product. We will also be implementing authentication with JSON Web Tokens (JWT) to secure our API.

Prerequisites

  • Basic knowledge of Laravel
  • Basic knowledge of REST APIs
  • Creating new project

    First we need to create a new laravel project. To create new LAravel project run below command in your terminal:
    1
    laravel new product-review-api

    After that you need to create your database and update the database credentials in the .env file.

    Create models and migrations

    For this project, we need three 3 models: user, product and review. By default laravel comes with a user model, so we have to create the remaining two. Let's start with the Product model:
    1
    php artisan make:model Product -a

    The -a flag will create corresponding migration, controller, factory and seeder file for our model.
    Now we have to edit the product migration file as below because we need to define the columns for the products table and defining its relationship with the users table and what happens if the parent data is deleted.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->longText('description');
    $table->decimal('price');
    $table->unsignedBigInteger('user_id');
    $table->timestamps();

    $table->foreign('user_id')
    ->references('id')
    ->on('users')
    ->onDelete('cascade');
    });

    Now we’ll need to create the Review model. To create Review model run below command in yout terminal:

    1
    php artisan make:model Review -a

    Now we have to edit the Review migration file as below because we need to define the columns for the products table and defining its relationship with the users and product table and what happens if the parent data is deleted.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Schema::create('reviews', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('user_id');
    $table->unsignedBigInteger('product_id');
    $table->text('review');
    $table->integer('rating');
    $table->timestamps();

    $table->foreign('user_id')
    ->references('id')
    ->on('users')
    ->onDelete('cascade');

    $table->foreign('product_id')
    ->references('id')
    ->on('products')
    ->onDelete('cascade');
    });

    Now, we have already finished with our Migration. Now we need to run the migrate artisan command.

    1
    php artisan migrate

    Model Relationships

    While we are writing our migrations, you have seen hat we have defined our relationship between the users, products and reviews tables. Let's see what relationship exists between this tables.
  • A user can add many products but a product can only belong to one user. This is a one to many relationship between user and product.
  • A product can have many reviews but a review can only belong to one product. This is a one to many relationship between product and review.
  • And finally user can make many reviews (be it same or different products) but a review can only belong to one user. This is a one to many relationship between the user and review.

  • Now we need to take this relationships to our Models.

    Let us define the relationship between the user and the other models.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <?php

    namespace App;

    use Illuminate\Contracts\Auth\MustVerifyEmail;
    use Illuminate\Foundation\Auth\User as Authenticatable;
    use Illuminate\Notifications\Notifiable;

    class User extends Authenticatable
    {
    use Notifiable;

    // Rest omitted for brevity

    /**
    * Get the products the user has added.
    */
    public function products()
    {
    return $this->hasMany('App\Product');
    }

    /**
    * Get the reviews the user has made.
    */
    public function reviews()
    {
    return $this->hasMany('App\Review');
    }
    }

    Now in the product model we need to define the relationship between the product and the review model, and also the inverse relationship to the user model.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Product extends Model
    {
    /**
    * Get the reviews of the product.
    */
    public function reviews()
    {
    return $this->hasMany('App\Review');
    }

    /**
    * Get the user that added the product.
    */
    public function user()
    {
    return $this->belongsTo('App\User');
    }
    }

    On the review model we will define the inverse relationship to the user and product model.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Review extends Model
    {
    /**
    * Get the product that owns the review.
    */
    public function product()
    {
    return $this->belongsTo('App\Product');
    }

    /**
    * Get the user that made the review.
    */
    public function user()
    {
    return $this->belongsTo('App\User');
    }
    }

    Database Seeding

    Database seeding means populating tables with data we want to develop. For this we use amazing library called Faker, without inputting this data manually. To do it paste below code in ProductFactory:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?php

    /** @var \Illuminate\Database\Eloquent\Factory $factory */

    use App\Product;
    use App\User;
    use Faker\Generator as Faker;

    $factory->define(Product::class, function (Faker $faker) {
    return [
    'name' => $faker->word,
    'description' => $faker->paragraph,
    'price' => $faker->numberBetween(1000, 20000),
    'user_id' => function() {
    return User::all()->random();
    },
    ];
    });

    And in ReviewFactory:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?php

    /** @var \Illuminate\Database\Eloquent\Factory $factory */

    use App\Review;
    use App\User;
    use Faker\Generator as Faker;

    $factory->define(Review::class, function (Faker $faker) {
    return [
    'review' => $faker->paragraph,
    'rating' => $faker->numberBetween(0, 5),
    'user_id' => function() {
    return User::all()->random();
    },
    ];
    });

    Next we need to write our seeders. Laravel comes with default user model, migration and factory files, but it doesn’t include a user seeder class. So we need to create User seeder by running the command:

    1
    php artisan make:seeder UserSeeder

    This will create a UserSeeder.php file in the database\seeds folder. Open that file and edit like this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * Run the database seeds.
    *
    * @return void
    */
    public function run()
    {
    factory(App\User::class, 50)->create();
    }

    We are using the factory helper method to insert 50 user records into the users table.

    To run our seeders, open the database\seeds\DatabaseSeeder.php file and uncomment the line

    1
    $this->call(UserSeeder::class);

    Then we can use the db:seed artisan command to seed the database. Head over to your users and you should see 50 user records created.

    1
    php artisan db:seed

    Let’s do the same for the ProductSeeder.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?php

    use Illuminate\Database\Seeder;

    class ProductSeeder extends Seeder
    {
    /**
    * Run the database seeds.
    *
    * @return void
    */
    public function run()
    {
    factory(App\Product::class, 100)->create()->each(function ($product) {
    $product->reviews()->createMany(factory(App\Review::class, 5)->make()->toArray());
    });
    }
    }

    It simply creating 100 products and for each product we are creating 5 reviews for that product.

    To call additional seeders in our DatabaseSeeder.php file, we pass an array instead to the call method

    1
    2
    3
    4
    $this->call([
    UserSeeder::class,
    ProductSeeder::class,
    ]);

    Then we should need to use migrate:fresh command, which will drop all tables and re-run all of our migrations. The --seedflag instructs laravel to seed the database once the migration is completed.

    1
    php artisan migrate:fresh --seed

    Now, we have already finished with our Database part. Le’s move on to Authentication part.

    Authentication

    To secure our application we are going to use the package jwt-auth for authentication with JSON Web Tokens (JWT). Run the below command in your terminal to install the package latest version:

    1
    composer require tymon/jwt-auth

    Note: If you are using Laravel 5.4 and below, you need to manually register the service provider. Add the service provider to the providers array in the config/app.php config file as follows:

    1
    2
    3
    4
    5
    6
    'providers' => [

    ...

    Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
    ]

    Next we need to publish it to the package config file:

    1
    php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

    Next, you need to run the below command to generate a secret key which we use to sign our tokens.

    1
    php artisan jwt:secret

    This will update your .env file with something like JWT_SECRET=value.

    Now we need to update the user model to implement the Tymon\JWTAuth\Contracts\JWTSubject contract, which requires we implement the 2 methods getJWTIdentifier() and getJWTCustomClaims().

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <?php

    namespace App;

    use Illuminate\Contracts\Auth\MustVerifyEmail;
    use Illuminate\Foundation\Auth\User as Authenticatable;
    use Illuminate\Notifications\Notifiable;
    use Tymon\JWTAuth\Contracts\JWTSubject;

    class User extends Authenticatable implements JWTSubject
    {
    // Rest omitted for brevity

    /**
    * Get the identifier that will be stored in the subject claim of the JWT.
    *
    * @return mixed
    */
    public function getJWTIdentifier()
    {
    return $this->getKey();
    }

    /**
    * Return a key value array, containing any custom claims to be added to the JWT.
    *
    * @return array
    */
    public function getJWTCustomClaims()
    {
    return [];
    }
    }

    Now we need to make a few changes to the config/auth.php.To use laravel’s built in auth system with jwt-auth.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
    ],

    ...

    'guards' => [
    'api' => [
    'driver' => 'jwt',
    'provider' => 'users',
    'hash' => false,
    ],
    ],

    Here we are telling the api guard to use the jwt driver, and setting the api guard as the default auth guard.

    Now, let’s move on to implementing the authentication logic our application. Let’s start by creating an AuthController

    1
    php artisan make:controller AuthController

    Let’s implement few methods in this controller.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    <?php

    namespace App\Http\Controllers;

    use App\User;
    use Illuminate\Http\Request;

    class AuthController extends Controller
    {
    public function __construct()
    {
    $this->middleware('auth')->except(['register', 'login']);
    }

    public function register(Request $request)
    {
    $request->validate([
    'name' => 'required|string',
    'email' => 'required|string|unique:users',
    'password' => 'required|string|min:8',
    ]);

    $user = User::create([
    'name' => $request->name,
    'email' => $request->email,
    'password' => bcrypt($request->password),
    ]);
    $token = auth()->login($user);
    return $this->respondWithToken($token);
    }

    public function login(Request $request)
    {
    $request->validate([
    'email' => 'required|string',
    'password' => 'required|string',
    ]);
    $credentials = $request->only(['email', 'password']);
    if (!$token = auth()->attempt($credentials)) {
    return response()->json(['error' => 'Invalid Credentials'], 401);
    }
    return $this->respondWithToken($token);
    }

    protected function respondWithToken($token)
    {
    return response()->json([
    'access_token' => $token,
    'token_type' => 'bearer',
    'expires_in' => auth()->factory()->getTTL() * 60
    ]);
    }

    /**
    * Get the authenticated User.
    *
    * @return \Illuminate\Http\JsonResponse
    */
    public function me()
    {
    return response()->json(auth()->user());
    }

    /**
    * Log the user out (Invalidate the token).
    *
    * @return \Illuminate\Http\JsonResponse
    */
    public function logout()
    {
    auth()->logout();
    return response()->json(['message' => 'Successfully logged out']);
    }

    }

    We use the auth middleware to only allow authenticated users to access to the application.

  • register method creates and stores a new user into the database.
  • login method allow user to log into the application.
  • respondWithToken() to return JWT response.
  • me to return the currently authenticated user.
  • logout to log out the currently authenticated user from the application.
  • Let's define our api routes. Open your routes/api.php and add the following routes.

    1
    2
    3
    4
    Route::get('me', 'AuthController@me');
    Route::post('login', 'AuthController@login');
    Route::post('register', 'AuthController@register');
    Route::post('logout', 'AuthController@logout');

    Product Endpoints

    Now we are going to work with our Product Endpoints.
  • GET /products - Fetch all products
  • GET /products/:id - Fetch a single product and its reviews
  • POST /products - Create a product
  • PUT /products/:id - Update a product
  • DELETE /products/:id - Delete a product
  • We can define this routes individually or use laravel resource routing feature in routes/api.php to define routes. In here we use the api resource route feature.

    Add the products routes in the route file:

    1
    Route::apiResource('products', 'ProductController');

    In the ProductController.php file paste below code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    <?php

    namespace App\Http\Controllers;

    use App\Product;
    use App\Review;
    use Illuminate\Http\Request;

    class ProductController extends Controller
    {
    public function __construct()
    {
    $this->middleware('auth')->except(['index', 'show']);
    }

    /**
    * Display a listing of the resource.
    *
    * @return \Illuminate\Http\Response
    */
    public function index()
    {
    $products = Product::with('user:id,name')
    ->withCount('reviews')
    ->latest()
    ->paginate(20);
    return response()->json(['products' => $products]);
    }

    /**
    * Store a newly created resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @return \Illuminate\Http\Response
    */
    public function store(Request $request)
    {
    $request->validate([
    'name' => 'required|string',
    'description' => 'required|string',
    'price' => 'required|numeric|min:0',
    ]);

    $product = new Product;
    $product->name = $request->name;
    $product->description = $request->description;
    $product->price = $request->price;

    auth()->user()->products()->save($product);
    return response()->json(['message' => 'Product Added', 'product' => $product]);
    }

    /**
    * Display the specified resource.
    *
    * @param \App\Product $product
    * @return \Illuminate\Http\Response
    */
    public function show(Product $product)
    {
    $product->load(['reviews' => function ($query) {
    $query->latest();
    }, 'user']);
    return response()->json(['product' => $product]);
    }

    /**
    * Update the specified resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @param \App\Product $product
    * @return \Illuminate\Http\Response
    */
    public function update(Request $request, Product $product)
    {
    if (auth()->user()->id !== $product->user_id) {
    return response()->json(['message' => 'Action Forbidden']);
    }
    $request->validate([
    'name' => 'required|string',
    'description' => 'required|string',
    'price' => 'required|numeric',
    ]);

    $product->name = $request->name;
    $product->description = $request->description;
    $product->price = $request->price;
    $product->save();

    return response()->json(['message' => 'Product Updated', 'product' => $product]);
    }

    /**
    * Remove the specified resource from storage.
    *
    * @param \App\Product $product
    * @return \Illuminate\Http\Response
    */
    public function destroy(Product $product)
    {
    if (auth()->user()->id !== $product->user_id) {
    return response()->json(['message' => 'Action Forbidden']);
    }
    $product->delete();
    return response()->json(null, 204);
    }
    }

    In the constructor, we use the auth middleware but excepting the index and show methods from using the middleware. It allows unauthenticated users to view all products and a single product.

  • index - returns a list of products ordered by the created date, the number of reviews the product has and paginates the records.
  • store - validates the request input and then creates a new product and attaches the product to the currently authenticated user and returns the newly created product.
  • show - returns a single product, the creator(user) and its reviews.
  • update - checks the currently authenticated user trying to update the record is the user that created the record. If the user is not the creator, a forbidden response is returned else we carry out the update operation.
  • delete - checks the currently authenticated user trying to delete the record is the user that created the record. If the user is not the creator, a forbidden response is returned else we carry out the delete operation.
  • Review Endpoints

    Let's begin by defining the review endpoints:
  • POST /products/:id/reviews - Create a review for a product
  • PUT /products/:id/reviews/:id - Update a product review
  • DELETE /products/:id/reviews/:id - Delete a product review
  • We define the apiResource route for the reviews and specifying actions the controller should handle instead of the full set of default actions.

    1
    2
    Route::apiResource('products/{product}/reviews', 'ReviewController')
    ->only('store', 'update', 'destroy');

    In the ReviewController.php file

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    <?php

    namespace App\Http\Controllers;

    use App\Product;
    use App\Review;
    use Illuminate\Http\Request;

    class ReviewController extends Controller
    {
    public function __construct()
    {
    $this->middleware('auth');
    }

    /**
    * Store a newly created resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @param \App\Product $product
    * @return \Illuminate\Http\Response
    */
    public function store(Request $request, Product $product)
    {
    $request->validate([
    'review' => 'required|string',
    'rating' => 'required|numeric|min:0|max:5',
    ]);

    $review = new Review;
    $review->review = $request->review;
    $review->rating = $request->rating;
    $review->user_id = auth()->user()->id;

    $product->reviews()->save($review);
    return response()->json(['message' => 'Review Added', 'review' => $review]);
    }

    /**
    * Update the specified resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @param \App\Product $product
    * @param \App\Review $review
    * @return \Illuminate\Http\Response
    */
    public function update(Request $request, Product $product, Review $review)
    {
    if (auth()->user()->id !== $review->user_id) {
    return response()->json(['message' => 'Action Forbidden']);
    }
    $request->validate([
    'review' => 'required|string',
    'rating' => 'required|numeric|min:0|max:5',
    ]);

    $review->review = $request->review;
    $review->rating = $request->rating;
    $review->save();

    return response()->json(['message' => 'Review Updated', 'review' => $review]);
    }

    /**
    * Remove the specified resource from storage.
    *
    * @param \App\Product $product
    * @param \App\Review $review
    * @return \Illuminate\Http\Response
    */
    public function destroy(Product $product, Review $review)
    {
    if (auth()->user()->id !== $review->user_id) {
    return response()->json(['message' => 'Action Forbidden']);
    }
    $review->delete();
    return response()->json(null, 204);
    }
    }

    Conclusion

    In this tutorial, we obtain how can we create built a simple api and covered authentication, database seeding and CRUD operation. You can obtain complete source code for this tutorial from this GitHub repository.

    Happy Coding