DEV Community

Cover image for How to create an API with laravel resources
Ted Ngeene
Ted Ngeene

Posted on • Edited on

How to create an API with laravel resources

In part 1 of this series we built a registration/login API for our app.
In this part of the series, we will continue with the laravel app by building a book review API that allows registered users to upload books, edit them, delete them as well as review their books and other books.
we shall be using laravel API resources.

what are API resources?

laravel API resources act as a transformation layer that sits between your Eloquent models and the JSON responses that are returned to your application's users. Laravel's resource classes allow you to expressively and easily transform your models and model collections into JSON.
For more insight into how they work you can check out the official laravel documentation.
Before we make the resources we'll be using, we have to define the models they will be implementing. For this guide, we will have Book, Rating and user models.
To make the Book model, run this command

php artisan make: model Book -m

the -m flag makes a corresponding migration file for the model. Repeat the process for the rating model. We don't need to make a user model since it comes off the shelf in laravel.
Next, we will edit the 'up' functions in the migrations we've just created. On the directory // database/migrations/TIMESTAMP_create_books_table.php edit it to look like this

class CreateBooksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->Integer('user_id')->unsigned();
            $table->string('title');
            $table->string('author');
            $table->text('description');
            $table->timestamps();

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

    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('books');
    }
}
Enter fullscreen mode Exit fullscreen mode

we define the field of the books table as well as the ID of the user who added the book. The user ID is a foreign key that references the 'id' field in the 'users' table.
Next, open the Rating migration, // database/migrations/TIMESTAMP_create_ratings_table.php and tweak it to look like this,

<?php

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

class CreateRatingsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('ratings', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedInteger('user_id');
            $table->unsignedInteger('book_id');
            $table->unsignedInteger('rating');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('ratings');
    }
}
Enter fullscreen mode Exit fullscreen mode

In this migration, we defined the fields that the ratings table will take, which are the ID,which is the primary key of the ratings table, the user_id of the user who posted a specific rating, the book_id to which the rating was posted, the rating itself(out 0f 5) and the timestamps the rating was created or updated.
After running all the above, let's run our migrations, but before we do, navigate to the app\providers\AppServiceProvider.php file and edit the boot function by adding the defaultStringthLength so that it looks like this

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Schema::defaultStringLength(191);
    }
}
Enter fullscreen mode Exit fullscreen mode

This will handle any migration errors usually caused by MariaDB(such as the one XAMPP has) or older versions of MySQL. Now that we've set it up, run php artisan migrate

Define Relationships between models

For the application logic of this API, let's understand how the models are related and the types of relationships we have to define for it to fit well together.

  1. A user can add many books but a book can only belong to one user -> this will be a one-to-many relationship between the User model and the Book model.
  2. A book can be rated by many users hence many ratings but a rating can only belong to one book -> this will be also a one-to-many relationship between the Book model and the rating model. Okay, now that I have explained that, let's define these relationships in our models. Open the app\User.php and add this function
public function books()
    {
        return $this->hasMany(Book::class);
    }
Enter fullscreen mode Exit fullscreen mode

Likewise, edit app\Book.php by adding

public function User()
    {
        return $this->belongsTo(User::class);
    }
    public function ratings()
    {
        return $this->hasMany(Rating::class);
    }
Enter fullscreen mode Exit fullscreen mode

Here, we have defined the relationship between the book model and the user model as well as the rating model. Next we will define the relationship between the rating model and the Book model by editing app/Rating.php file to look like this

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Rating extends Model
{
    public function book()
    {
        return $this->belongsTo(Book::class);
    }
    protected $fillable = ['book_id','user_id','rating'];
}
Enter fullscreen mode Exit fullscreen mode

Apart from defining the relationship, we have allowed mass assignment of the fields that the rating model will accept, which are the book_id,user_id and the rating which the users have to fill.
For the Book model, we will allow mass assignment by adding

 protected $fillable = ['user_id','title','author','description'];
Enter fullscreen mode Exit fullscreen mode

Create the Book Resource

Making resource classes in laravel is simplified by using artisan commands. To make the Book resource class, run php artisan make:Resource BookResource
which is created in app/Http/Resources/BookResource.php directory. Navigate here and edit the toArray() method so that it looks like this:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class BookResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        //transforms the resource into an array made up of the attributes to be converted to JSON
        return [
            'id'=> $this->id,
            'title'=> $this->title,
            'author' => $this->author,
            'description'=>$this->description,
            'created_at' => (string) $this->created_at,
            'updated_at' => (string) $this->updated_at,
            'user' => $this->user,
            'ratings' => $this->ratings,
            'average_rating' => $this->ratings->avg('rating')
        ];

    }
}
Enter fullscreen mode Exit fullscreen mode

Every resource class defines a toArray method which returns the array of attributes that should be converted to JSON when sending the response. All the attributes of the Book Model we need converted to JSON are defined, we can also remove those attributes we do not need to be serialized to JSON. Notice that we can access model properties directly from the $this variable. This(pun intended) is because a resource class will automatically proxy property and method access down to the underlying model for convenient access. Once the resource is defined, it may be returned from a route or controller. Now we can make use of the BookResource class in our BookController which we will now make.

Creating the Book Controller

Our BookController will make use of the API controller generation feature, which is defined by php artisan make:controller BookController --api
open this controller, located in app\Http\Controller\BookController.php and paste this

<?php

namespace App\Http\Controllers;

use App\Book;
use Illuminate\Http\Request;
use App\Http\Resources\BookResource;

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

    /**
     * Display all books that have been added
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
      return BookResource::collection(Book::with('ratings')->paginate(25));
    }
  /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $this->validate($request, [
            'title' => 'required',
            'description' => 'required',
            'author' => 'required',
        ]);
        $book = new Book;
        $book->user_id = $request->user()->id;
        $book->title = $request->title;
        $book->description = $request->description;
        $book->author = $request->author;
        $book->save();

        return new BookResource($book);
}
    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show(book $book)
    {
        return new BookResource($book);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    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','author', 'description']));



      return new BookResource($book);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy(Request $request,Book $book)
    {
      if($request->user()->id != $book->user_id){
        return response()->json(['error' => 'You can only delete your own books.'], 403);
      }
        $book ->delete();
        return response()->json(null,204);
    }
}
Enter fullscreen mode Exit fullscreen mode

The index() method fetches and returns a list of all the books that have been added. We are making use of the BookResource created earlier.
The store() method creates a new book with the ID of the currently authenticated user along with the details of the book. Before the book is stored it has to be validated that all the fields have been entered, hence the validation check. A BookResource is returned based on the newly created book.
The show() method returns a book resource based on the specified book.
The update() method first checks to make sure that the user trying to update a book is the user who created it. If not, we return a message that one can only edit their book. If the person is the owner, they can update the book with the new details and return a book resource with the updated details.
The destroy() method deletes a specified book from the database. However, one can only deleted a book they uploaded. Therefore we set the HTTP status code 403 with an error message notifying one that they can only delete their own books if they try to delete other people's books. If the user is the owner of the book, we set the HTTP status code of the response returned to 204, which indicates that there is no content to return since the specified resource has been deleted.
Also, note that we have secured our API endpoints by using the auth::api middleware on the __construct function. We are exempting the index and show methods from implementing the middleware. This way, unauthenticated users will be able to see a list of all books and a particular book.

Defining API routes

Finally, we will define the API routes. open routes\api.php and add the following

Route::apiResource('books', 'BookController');
Enter fullscreen mode Exit fullscreen mode

We are making use of apiResource() to generate API only routes. It is recommended to use this method as it will generate only API specific routes(index, store, show, update and destroy). Unlike using the normal resource() method which, in addition to generating the API specific routes, also generates create() and edit() routes which are aren't necessary when building an API.

That's it for part 2 of building an API with laravel, I had hoped to finish with 3 parts but due to the volume of content, I'll have to add more parts, which will cover the rating functionality as well as a final part that will cover testing of the app to make sure that it actually does work. So don't go anywhere :-)
I'd like to mention that the inspiration for this came from this post I found on pusher.com which is a good starting point. I found some errors, I'm trying to fix by writing this series.
You can also find the source code on my github

Sponsors

Please note that some of the links below are affiliate links. I only recommend products, tools and learning services I've personally used and believe are genuinely helpful. Most of all, I would never advocate for buying something you can't afford or that you aren't ready to use.

  • Scraper API is a startup specializing in strategies that'll ease the worry of your IP address from being blocked while web scraping. They utilize IP rotation so you can avoid detection. Boasting over 20 million IP addresses and unlimited bandwidth. Using Scraper API and a tool like 2captcha will give you an edge over other developers. The two can be used together to automate processes. Sign up on Scraper API and use this link to get a 10% discount on your first purchase.

  • Do you need a place to host your website or app, Digital ocean
    is just the solution you need, sign up on digital ocean using this link and experience the best cloud service provider.

  • The journey to becoming a developer can be long and tormentous, luckily Pluralsight makes it easier to learn. They offer a wide range of courses, with top quality trainers, whom I can personally vouch for. Sign up using this link and get a 50% discount on your first course.

Top comments (6)

Collapse
 
bangtranit profile image
bangtranit

thanks for great tutorial.
i just called the create post api by postman but it return for me this error. How can i resolve this error? Please help me. Thank you

<!DOCTYPE html>
<!--

InvalidArgumentException: Route [login] not defined. in file /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php on line 388
Stack trace:

  1. InvalidArgumentException->() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php:388
  2. Illuminate\Routing\UrlGenerator->route() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:782
  3. route() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/app/Http/Middleware/Authenticate.php:18
  4. App\Http\Middleware\Authenticate->redirectTo() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php:68
  5. Illuminate\Auth\Middleware\Authenticate->authenticate() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php:41
  6. Illuminate\Auth\Middleware\Authenticate->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:163
  7. Illuminate\Pipeline\Pipeline->Illuminate\Pipeline{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
  8. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php:58
  9. Illuminate\Routing\Middleware\ThrottleRequests->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:163
    1. Illuminate\Pipeline\Pipeline->Illuminate\Pipeline{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
    2. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:104
    3. Illuminate\Pipeline\Pipeline->then() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php:682
    4. Illuminate\Routing\Router->runRouteWithinStack() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php:657
    5. Illuminate\Routing\Router->runRoute() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php:623
    6. Illuminate\Routing\Router->dispatchToRoute() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php:612
    7. Illuminate\Routing\Router->dispatch() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:176
    8. Illuminate\Foundation\Http\Kernel->Illuminate\Foundation\Http{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:30
    9. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:21
    10. Illuminate\Foundation\Http\Middleware\TransformsRequest->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:163
    11. Illuminate\Pipeline\Pipeline->Illuminate\Pipeline{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
    12. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:21
    13. Illuminate\Foundation\Http\Middleware\TransformsRequest->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:163
    14. Illuminate\Pipeline\Pipeline->Illuminate\Pipeline{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
    15. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php:27
    16. Illuminate\Foundation\Http\Middleware\ValidatePostSize->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:163
    17. Illuminate\Pipeline\Pipeline->Illuminate\Pipeline{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
    18. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php:62
    19. Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:163
    20. Illuminate\Pipeline\Pipeline->Illuminate\Pipeline{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
    21. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/fideloper/proxy/src/TrustProxies.php:57
    22. Fideloper\Proxy\TrustProxies->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:163
    23. Illuminate\Pipeline\Pipeline->Illuminate\Pipeline{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
    24. Illuminate\Routing\Pipeline->Illuminate\Routing{closure}() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:104
    25. Illuminate\Pipeline\Pipeline->then() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:151
    26. Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:116
    27. Illuminate\Foundation\Http\Kernel->handle() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/public/index.php:55
    28. require_once() /Users/bang.tran/Desktop/phplaravelproject/jwtauth-laravel/server.php:21
Collapse
 
tngeene profile image
Ted Ngeene • Edited

Hi..thanks for following through. the error might be because you haven't defined the login route in routes\api.php file. copy-paste the following and tell me if it fixes the issue Route::post('login', 'AuthController@login');

Collapse
 
bangtranit profile image
bangtranit

Nice. Thanks for reply.
As the your suggest i added login route to the api.php and it was resolved.
thank you so much.

Thread Thread
 
tngeene profile image
Ted Ngeene • Edited

You're welcome... I'll be uploading the rest of the series as well as how to integrate the whole API with a vue.js front end

Collapse
 
mahmoudz profile image
Mahmoud Zalt

Nice article! I'm using Apiato (apiato.io), it's a full API framework built on top of Laravel.

Collapse
 
tngeene profile image
Ted Ngeene

thank you!! I have checked it out. Rock-solid documentation and I will try out.