Laravel

Build a REST API with Laravel API resources

This tutorial shows how to use the Laravel API resources feature to build a REST API. API resources were introduced in Laravel 5.5. Before the introduction of API resources, we often used a package like fractal as a transformation layer to output JSON responses when building REST APIs. So, in this tutorial, I’ll be showing how to build a robust API in Laravel using API resources.

Prerequisites

This tutorial assumes you already have the following:

  • A basic knowledge of Laravel.
  • A basic knowledge of REST APIs.
  • Have the Laravel installer installed on your computer.

What are API resources

API resources present a way to easily transform our models into JSON responses. It acts as a transformation layer that sits between our Eloquent models and the JSON responses that are actually returned by our API. API resources is made of two entities: a resource class and a resource collection. A resource class represents a single model that needs to be transformed into a JSON structure, while a resource collection is used for transforming collections of models into a JSON structure.

Both the resource class and the resource collection can be created using artisan commands:

 // create a resource class
    $ php artisan make:resource UserResource

    // create a resource collection using either of the two commands
    $ php artisan make:resource Users --collection
    $ php artisan make:resource UserCollection

What we’ll be building

For the purpose of this demonstration, we’ll be building a book reviews API. Users will be able to add new books, update books and delete books. Users will also be able to view a list of all books and rate a book. Then an average rating will be computed based on the ratings on a particular book. Finally, we’ll add authentication with JSON Web Tokens (JWT) to make the API secured.

Create a new Laravel app

We’ll start by creating a new Laravel app, I’ll be making use of the Laravel installer:

    $ laravel new book-reviws-api

Create models and migrations

The book reviews API will have three models: User, Book and Rating. Luckily for us, a User model already comes with Laravel by default. So, we’ll create the remaining two and their corresponding migrations. We’ll start by creating the Book model:

    $ php artisan make:model Book -m

? The -m flag will create the corresponding migration file for the model.

Next, let’s open the migration file generated for the Book model and update the up() the method as below:

    // database/migrations/TIMESTAMP_create_books_table.php

    public function up()
    {
      Schema::create('books', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedInteger('user_id');
        $table->string('title');
        $table->text('description');
        $table->timestamps();
      });
    }

We define the fields for the books table which are an auto increment ID, the ID of the user that added the book, the title of the book and the description of the book. Then some timestamps (created_at and updated_at).

We’ll do the same for the Rating model:

    $ php artisan make:model Rating -m

Open the migration file generated for the Rating model and update the up() method as below:

    // database/migrations/TIMESTAMP_create_ratings_table.php

    public function up()
    {
      Schema::create('ratings', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedInteger('user_id');
        $table->unsignedInteger('book_id');
        $table->unsignedInteger('rating');
        $table->timestamps();
      });
    }

We define the fields for the ratings table which are an auto increment ID, the ID of the user that rated the book, the ID of the book that was rated and the rating itself (ranging from 0-5). Then some timestamps (created_at and updated_at).

Run the the command below to run the migrations:

    $ php artisan migrate

Remember to enter your database details in the .env file before running the command above.

Define relationships between models

A user can add as many books as they wish, but a book can only belong to one user. So, the relationship between the User model and Book model is a one-to-many relationship. Let’s define that. Add the code below inside the User model:

    // app/User.php

    public function books()
    {
      return $this->hasMany(Book::class);
    }

Next, let’s define the inverse relationship on the Book model:

    // app/Book.php

    public function user()
    {
      return $this->belongsTo(User::class);
    }

Likewise, a book can be rated by various users, hence a book can have many ratings. A rating can only belong to one book. This is also a one-to-many relationship. Add the code below in the Book model:

    // app/Book.php

    public function ratings()
    {
      return $this->hasMany(Rating::class);
    }

Then we define the inverse relationship inside the Rating model:

    // app/Rating.php

    public function book()
    {
      return $this->belongsTo(Book::class);
    }

Allowing mass assignment on some fields

We’ll be using the create() method to save new model in a single line. To avoid getting the mass assignment error which Laravel will throw by default, we need to specify the columns we want to be mass assigned. To do this, let’s add the snippet below to our models respectively:

    // app/Book.php

    protected $fillable = ['user_id', 'title', 'description'];
    // app/Rating.php

    protected $fillable = ['book_id', 'user_id', 'rating'];

Adding user authentication

As already mentioned, we’ll be securing our API by adding user authentication with JWT. For this, we’ll make use of a package called jwt-auth. Let’s install and set it up:

    $ composer require tymon/jwt-auth "1.0.*"

Note: If you are using Laravel 5.4 and below, you will need to manually register the service provider by adding it in the providers array in your app.php config file.

Once that’s done installing, let’s run the command below to publish the package’s config file:

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

This will create a config/jwt.php file that will allow us to configure the basics of the package.

Next, run the command below to generate a secret key:

    $ php artisan jwt:secret

This will update the .env file with something like JWT_SECRET=some_random_key. This key will be used to sign our tokens.

Before we can start to use the jwt-auth package, we need to update our User model to implement the Tymon\JWTAuth\Contracts\JWTSubject contract as below:

    // app/User.php

    use Tymon\JWTAuth\Contracts\JWTSubject;

    class User extends Authenticatable implements JWTSubject
    {
      ...
    }

This requires that we implement two methods: getJWTIdentifier() and getJWTCustomClaims(). So add the code below to the User model:

    // app/User.php

    public function getJWTIdentifier()
    {
      return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
      return [];
    }

The first method gets the identifier that will be stored in the subject claim of the JWT and the second method allow us to add any custom claims we want added to the JWT. We won’t be adding any custom claims in this tutorial.

Next, let’s configure the auth guard to make use of the jwt guard. Update config/auth.php as below:

    // config/auth.php

    'defaults' => [
      'guard' => 'api',
      'passwords' => 'users',
    ],

    ...

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

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

Now we can start to make use of the jwt-auth package. Create a new AuthController:

    $ php artisan make:controller AuthController

Then paste the code below into it:

    // app/Http/Controllers/AuthController.php

    // remember to add this to the top of the file
    use App\User;

    public function register(Request $request)
    {
      $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)
    {
      $credentials = $request->only(['email', 'password']);

      if (!$token = auth()->attempt($credentials)) {
        return response()->json(['error' => 'Unauthorized'], 401);
      }

      return $this->respondWithToken($token);
    }

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

We define the methods to register a new user and to log users in respectively. Both methods returns a response with a JWT by calling a respondWithToken() method which gets the token array structure.

Next, let’s add the register and login routes. Add the code below inside routes/api.php:

    // routes/api.php

    Route::post('register', 'AuthController@register');
    Route::post('login', 'AuthController@login');

Defining API routes

Let’s define our routes. Open routes/api.php and add the line below to it:

    // routes/api.php

    Route::apiResource('books', 'BookController');
    Route::post('books/{book}/ratings', 'RatingController@store');

Since we are building an API, we make use of apiResource() to generate API only routes. Also, we define a route that will be used to rate a specified book. For instance, /books/53/ratings will be used to rate the book with the ID of 53.

Tips: When building APIs with Laravel, it is recommended to use the apiResource() method while defining resourceful routes, this will generate only API specific routes (indexstoreshowupdate and destroy). Unlike when you use the resource() method, which will in addition to generating API specific routes, also generate create and edit routes, which aren’t needed when building an API.

Creating the book resource

Before we move on to create the BooksController, let’s create a book resource class. We’ll make use of the artisan command make:resource to generate a new book resource class. By default, resources will be placed in the app/Http/Resources directory of our application.

    $ php artisan make:resource BookResource

Once that is created, let’s open it and update the toArray() method as below:

    // app/Http/Resources/BookResource.php

    public function toArray($request)
    {
      return [
        'id' => $this->id,
        'title' => $this->title,
        'description' => $this->description,
        'created_at' => (string) $this->created_at,
        'updated_at' => (string) $this->updated_at,
        'user' => $this->user,
        'ratings' => $this->ratings,
      ];
    }

As the name suggests, this will transform the resource into an array. The array is made up of the attributes we want to be converted to JSON when sending the response. So the response will, in addition to containing the details about a book, also contain the user that added the book and all the ratings of the book. Any details we don’t want included in the JSON response, we simply remove it from the toArray() method. You’ll notice we are casting the dates (created_at and update_at) to strings because otherwise the dates will be returned as objects in the response.

As you can see, we can access the model properties directly from the $this variable because a resource class will automatically proxy property and method access down to the underlying model for convenient access. Now we can make use of the BookResource class in our controller.

Creating the book controller

Let’s create the BookController. For this, we’ll make use of the API controller generation feature that was introduced in the Laravel 5.6.

    $ php artisan make:controller BookController --api

Next, open it up and paste the following code into it:

    // app/Http/Controllers/BookController.php

    // add these at the top of the file
    use App\Book;
    use App\Http\Resources\BookResource;

    public function index()
    {
      return BookResource::collection(Book::with('ratings')->paginate(25));
    }

    public function store(Request $request)
    {
      $book = Book::create([
        'user_id' => $request->user()->id,
        'title' => $request->title,
        'description' => $request->description,
      ]);

      return new BookResource($book);
    }

    public function show(Book $book)
    {
      return new BookResource($book);
    }

    public function update(Request $request, Book $book)
    {
      // check if currently authenticated user is the owner of the book
      if ($request->user()->id !== $book->user_id) {
        return response()->json(['error' => 'You can only edit your own books.'], 403);
      }

      $book->update($request->only(['title', 'description']));

      return new BookResource($book);
    }

    public function destroy(Book $book)
    {
      $book->delete();

      return response()->json(null, 204);
    }

The index() method fetches and returns a list of the books that have been added. We are making use of the BookResource created earlier. Because we are fetching a list of books, we make use of the collection() which is available on the resource class. This allows us to fetch a collection of resources. We could have a created an additional resource collection (e.g. php artisan make:resource BookCollection) which will allow us to customize the meta data returned with the collection, but since we won’t be customizing the meta data returned we’ll just stick with the collection().

The store() method creates a new book with the ID of the currently authenticated user along with the details of the book, and persists it to the database. Then we return a book resource based on the newly created book.

The show() method accepts a Book model (we are using route model binding here) and simply returns a book resource based on the specified book.

The update() method first checks to make sure the user trying to update a book is the owner of the book (that is, the user is the one who created the book). If the user is not the owner of the book, we return an appropriate error message and set the HTTP status code to 403 (which indicates: Forbidden – the user is authenticated, but does not have the permissions to perform an action). Otherwise we update the book with the new details and return a book resource with the updated details.

Lastly, the destroy() method deletes a specified book from the database. Since the specified book has been deleted and no longer available, we set the HTTP status code of the response returned to 204 (which indicates: No content – the action was executed successfully, but there is no content to return).

Creating the rating resource

Just as we did with the BookResource, we’ll also create a rating resource class:

    $ php artisan make:resource RatingResource

Once that is created, let’s open it and update the toArray() method as below:

    // app/Http/Resources/RatingResource.php

    public function toArray($request)
    {
      return [
        'user_id' => $this->user_id,
        'book_id' => $this->book_id,
        'rating' => $this->rating,
        'created_at' => (string) $this->created_at,
        'updated_at' => (string) $this->updated_at,
        'book' => $this->book,
      ];
    }

Again, we pass along the attributes we want to be converted to JSON when sending the response. The response will also contain the book the rating is for.

Creating the rating controller

Next, create the RatingController that will make use of the RatingResource:

    $ php artisan make:controller RatingController

Next, open it up and paste the following code into it:

    // app/Http/Controllers/RatingController.php

    // add these at the top of the file
    use App\Book;
    use App\Rating;
    use App\Http\Resources\RatingResource;

    public function store(Request $request, Book $book)
    {
      $rating = Rating::firstOrCreate(
        [
          'user_id' => $request->user()->id,
          'book_id' => $book->id,
        ],
        ['rating' => $request->rating]
      );

      return new RatingResource($rating);
    }

The store() is used to rate a specified book. We are using the firstOrCreate() which checks if a user has already rated a specified book. If the user has, we simply return a rating resource based on the rating. Otherwise, we add the user rating to the specified book and persist it to the database. Then we return a rating resource based on the newly added rating.

Getting average rating

As it stands, we are almost done with all the features for our API. The last feature that’s left is getting the average rating made on a book. This is straightforward to do since the API is already well structured.

Add the line of code below to the toArray() method of app/Http/Resources/BookResource.php:

    // app/Http/Resources/BookResource.php

    'average_rating' => $this->ratings->avg('rating')

We are using the ratings relationship defined of the Book model to fetch all the ratings that have be made on the specified book. Then, using collection avg(), we get the average of the ratings. Passing rating to the avg() function indicates that we want to calculate the average based on the book rating.

Now, whenever the BookResource is used, the response will contain the average rating of the book.

A sample book resource response will look like below:

book response preview

A sample rating resource response will look like below:
rating response preview

You can see how the responses are well-formatted.

Securing the API endpoints

Before we wrap up this tutorial, let’s secure our API endpoints using middleware. To secure the books endpoint, add the code below to app/Http/Controllers/BookController.php:

    // app/Http/Controllers/BookController.php

    public function __construct()
    {
      $this->middleware('auth:api')->except(['index', 'show']);
    }

As you can see, we are making use of the auth:api middleware. Here, we are exempting the index() and show() methods from using the middleware. That way, users will be able to see a list of all books and a particular book without needing to be authenticated.

Let’s also secure the endpoint to rate a book, add the code below to app/Http/Controllers/RatingController.php:

    // app/Http/Controllers/RatingController.php

    public function __construct()
    {
      $this->middleware('auth:api');
    }

Handling resource not found

By default when a specified model is not found, Laravel will throw a ModelNotFoundException and renders a 404 page. Since we are building an API, we want to handle the exception and throw an API friendly error message.

Add the code below to the render) method of app/Exceptions/Handler.php:

    // app/Exceptions/Handler.php

    if ($exception instanceof ModelNotFoundException && $request->wantsJson()) {
      return response()->json([
        'error' => 'Resource not found'
      ], 404);
    }

This checks if the exception thrown is an instance of ModelNotFoundException and the request wants JSON, then we simply return a response with an error message of Resource not found and set the HTTP status code to 404 (which indicates: Not Found – the server has not found anything matching the Request-URI).

Tips: For the above to work, the API requests will need the header Accept: application/json.

Shaiv Roy

Hy Myself shaiv roy, I am a passionate blogger and love to share ideas among people, I am having good experience with laravel, vue js, react, flutter and doing website and app development work from last 7 years.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button