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');
}
}
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');
}
}
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);
}
}
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.
- 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. - 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 theapp\User.php
and add this function
public function books()
{
return $this->hasMany(Book::class);
}
Likewise, edit app\Book.php
by adding
public function User()
{
return $this->belongsTo(User::class);
}
public function ratings()
{
return $this->hasMany(Rating::class);
}
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'];
}
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'];
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')
];
}
}
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);
}
}
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');
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)
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:
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 issueRoute::post('login', 'AuthController@login');
Nice. Thanks for reply.
As the your suggest i added login route to the api.php and it was resolved.
thank you so much.
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
Nice article! I'm using Apiato (apiato.io), it's a full API framework built on top of Laravel.
thank you!! I have checked it out. Rock-solid documentation and I will try out.