API Development with Laravel 10 – Build a REST API for a Bookstore
Step 1: Setting Up the Environment
Let’s get started building! Here’s a step-by-step guide on installing Laravel 10, setting up the database, and creating the basic CRUD operations for our Bookstore API.
Prerequisites:
- PHP: Ensure you have PHP version 8.1 or above installed on your system. You can check your version on the command line with php -v.
- Composer: Composer is the dependency manager for PHP. You can download and install it from https://getcomposer.org/ You can check the version using this command line composer -v
Steps:
- Install Laravel 10:
- Open your terminal or command prompt.
- Navigate to the directory where you want to create your project (e.g., cd Documents)
- Run the following command to create a new Laravel project:
composer create-project laravel/laravel bookstore-api
composer create-project laravel/laravel mybookstore "10.*"
What is a Virtual Host?
- A virtual host is a configuration that allows a single web server (like Apache) to host multiple websites or applications.
- Each virtual host has its own:
- DocumentRoot: The directory where the project’s files are located.
- ServerName: A domain name or alias that points to the specific project.
- Error Logs: Separate error and access logs for each project
How To Create a Virtual Host
The process depends on your web server (Apache, Nginx, etc.) and operating system. Here’s a general outline for Apache:
- Create New Virtual Host Configuration File:
Open your main Apache configuration file (httpd.conf), located in your XAMPP installation directory (e.g., C:\xampp\apache\conf\httpd.conf).
-
- Find the line that looks approximately like this:
# Virtual hosts
Include conf/extra/httpd-vhosts.conf
-
- Paste the following template and modify accordingly:
-
# Virtual hosts Include conf/extra/httpd-vhosts.conf <VirtualHost *:80> DocumentRoot "/path/to/your/project/public" ServerName yourproject.local <Directory "/path/to/your/project/public"> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> ErrorLog "logs/yourproject-error.log" CustomLog "logs/yourproject-access.log" common </VirtualHost>
Update Your hosts File:
- Add an entry to map yourproject.local to your local IP address (127.0.0.1). You’ll find this file at:
- Windows: C:\Windows\System32\drivers\etc\hosts
- Linux/macOS: /etc/hosts
- Add an entry to map yourproject.local to your local IP address (127.0.0.1). You’ll find this file at:
On your Laravel app update your app_url in the .env file like this
APP_URL=http://bookstore-api.local
- Restart Apache:
-
- Click “Stop” next to Apache, and then click “Start” to restart the web server on windows
-
- Use the appropriate command for your system, e.g., sudo service apache2 restart.
Run the app using this url: http://bookstore-api.local/
- Set Up Database:
- Create Database: Create a database named ‘bookstore’ in your preferred database management system (MySQL, PostgreSQL, etc.).
- Update Environment File:
- Go to the root of your project folder (bookstore-api) and find the .env file.
- Update the following lines with your database connection details:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=bookstore
DB_USERNAME=your_database_username
DB_PASSWORD=your_database_password
- Create a Model and Migration:
- Run the following command to create a Book model and its corresponding migration:
php artisan make:model Book -m
- Open the newly created migration file within the database/migrations folder. Define the schema for your books table:
public function up(): void
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('author');
$table->string('isbn')->unique();
$table->decimal('price', 8, 2);
$table->timestamps();
});
}
Run the migration:
php artisan migrate
Layout of The Project
- Before go to step two let me share with you the layout of the project are creating.
- Since we’re focused purely on building an API (no fancy visual frontend this time), the ‘layout’ will primarily be in the form of a conceptual diagram and a description of the data flow.
Conceptual Diagram
[User/Client] --(HTTP Requests)--> [Laravel API (Bookstore)] --(Database Queries)--> [Bookstore Database]
Explanation
- User/Client: This represents any application that is going to use your API. It could be a website, a mobile app, or even another API. For this tutorial, we’ll use Postman as the ‘client’ to make requests.
- HTTP Requests: The client will communicate with our Laravel API using standard HTTP requests:
- GET (to retrieve data)
- POST (to create new data)
- PUT (to update data)
- DELETE (to delete data)
- Laravel API (Bookstore): This is the heart of our project, the code we’ll write in Laravel:
- Routes will define URLs the client can access.
- Controllers will handle requests and interact with the database.
- Bookstore Database: A database to store and manage the book data (like title, author, ISBN, price).
Data Flow Example (GET Request)
- A user wants to see a list of books. Their frontend application (or Postman) will send a GET request to /api/books
- Laravel API routes this request to the BookController.
- The BookController queries the ‘books’ database table to fetch all book records.
- The database returns the data.
- The BookController formats the data as JSON.
- The Laravel API sends the JSON response back to the client.
- The client renders the data for the user to see.
Step 2: Create the Controller to handle the API logic and Set up the routes
Let’s create the controller and set up the API routes for our Bookstore.
- Create the Controller
- Use the artisan command to generate an API controller:
php artisan make:controller Api/BookController --resource
- The –resource flag automatically generates the methods we need for basic CRUD
- Update the Controller
-
- Open the generated controller file (app/Http/Controllers/Api/BookController.php).
- Update the controller code as follows:
<?php
namespace App\Http\Controllers\Api;
use App\Models\Book;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class BookController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return Book::all(); // Get all books
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$book = Book::create($request->all()); // Create a new book
return response()->json($book, 201);
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
$book = Book::find($id);
if (!$book) { // Check if the book is found
return response()->json(['message' => 'Book not found'], 404);
}
return $book;
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
$book = Book::find($id);
if (!$book) {
return response()->json(['message' => 'Book not found'], 404);
}
$book->update($request->all());
return response()->json($book, 200);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
$book = Book::find($id);
if (!$book) {
return response()->json(['message' => 'Book not found'], 404);
}
$book->delete();
return response()->json(['message' => 'Book deleted'], 200);
}
}
- Set up the routes:
- Open the routes/api.php file.
- Add the following line:
Route::resource('books', BookController::class)->except(['create', 'edit']);
Note: The resource method typically includes create and edit routes, however, these are commonly used for building front-facing forms within a web application. In a pure API context, it’s up to you whether you want to exclude them:
Use Postman:
- Make a GET request to http://bookstore-api.local/api/books.
- You should receive an empty array ([]) since your database is empty.
Explanation:
Route::resource(‘books’, BookController::class): This line automatically generates the following routes:
| Method | URI | Action | Route Name |
| ------ | ----------------- | -------- | ------------ |
| GET | /books | index | books.index |
| GET | /books/create | create | books.create |
| POST | /books | store | books.store |
| GET | /books/{book} | show | books.show |
| GET | /books/{book}/edit| edit | books.edit |
| PUT | /books/{book} | update | books.update |
| DELETE | /books/{book} | destroy | books.destroy|
Step 3: Let’s Test Our Bookstore API
Let’s move on to testing our Bookstore API with Postman! Here’s a step-by-step guide:
- Setting up Postman:
- Download: If you haven’t already, download and install Postman from https://www.postman.com/.
- Workspace: Create a new workspace in Postman and give it a descriptive name (e.g., “Bookstore API Testing”).
- Testing the Endpoints
GET /api/books (Retrieve all books):
-
- Method: In Postman, set the request type to GET.
- URL: Enter the URL: http://bookstore-api.local/api/books (or the URL where your Laravel app is running).
- Send: Click the “Send” button. You should see an empty array response ([]) since you don’t have any books in your database yet.
POST /api/books (Create a new book):
-
- Method: Select POST.
- URL: http://bookstore-api.local/api/books
- Body: Select the “Body” tab, then the “raw” option, and select “JSON” from the dropdown.
- Enter JSON Data: Paste in JSON data for a book:
{
"title": "The Hitchhiker's Guide to the Galaxy",
"author": "Douglas Adams",
"isbn": "978-0345391803",
"price": 12.99
}
- Send: Click the “Send” button. You should see the newly created book in the response with a status code of 201 (Created).
GET /api/books/{id} (Get a single Book)
-
- Get the id of the book you just created from the previous response.
- Change the request method to GET.
- URL: http://bookstore-api.local/api/books/{id} (replace {id} with the actual ID).
- Send: Click the “Send” button. You should see the details of the book in the response.
(Modify the book data as needed and test…)
PUT /api/books/{id} (Update a book)
- Get the ID:Retrieve the id of an existing book from your database (either from a previous GET request or by directly looking at the database).
- Method:Select the PUT
- URL:http://bookstore-api.local/api/books/{id} (replace {id} with the actual book ID).
- Body:
-
- Select the “Body” tab and ensure “raw” and “JSON” are selected just as you did for the POST request.
- Paste in a JSON object representing the updated book data. Modify only the fields you want to change. Example:
{
"title": "The Hitchhiker's Guide to the Galaxy (Updated Edition)",
"price": 13.99
}
- Send: Hit the “Send” button.
- You should receive a response with the updated book data and a status code of 200 (OK).
DELETE /api/books/{id} (Delete a book)
- Get the ID: Retrieve the id of a book you wish to delete.
- Method: Change the request method to DELETE.
- URL: http://bookstore-api.local/api/books/{id} (replace {id} with the actual book ID).
- Body: You don’t need a body for a DELETE request.
- Send: Click the “Send” button.
- You should get a 200 (OK) status code, and a success message in the response (like the one you defined in your controller).
Authentication and Authorization
- Authentication (Using Laravel Sanctum)
- Laravel Sanctum is a fantastic solution for API authentication. Here’s a step-by-step guide on how to implement this:
Step 1: Install and Configure Laravel Sanctum
1.Install Sanctum:
composer require laravel/sanctum
2.Publish Sanctum’s configuration:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
3.Run database migrations:
php artisan migrate
Step 2: Modify your User Model
- Add the necessary trait:
Import the Laravel\Sanctum\HasApiTokens trait into your User Model.
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
// ... other user model code
}
Add the ‘role’ Column:
- Generate a migration:
php artisan make:migration add_role_column_to_users_table
Edit the migration file: Open the newly created migration file within your database/migrations directory. Add the following code within the up method:
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('role')->default('user'); // Set the default role as 'user'
});
}
Run the migration:
php artisan migrate
Also update the User model by adding a role field in the protected fillable array.
protected $fillable = [
'name',
'email',
'password',
'role',
];
Step 3: Create API Routes for Authentication
- We’ll need routes for login, registration and logout. You can put these in either your routes/api.php file or create a separate file. Here’s an example:
Route::prefix('auth')->group(function () {
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); // Protected route
});
Step 4: Create an AuthController
- Generate the controller:
php artisan make:controller Api/AuthController
Implement methods: Add register, login, and logout methods inside your AuthController. You’ll need basic user creation logic and Sanctum’s token creation features in these methods.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class AuthController extends Controller
{
public function register(Request $request)
{
// Basic validation of incoming data
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
'role' => 'required|string|in:admin,user', // Ensure the 'role' field is included and valid
]);
if ($validator->fails()) {
return response()->json(['error' => $validator->errors()], 422); // Return validation errors
}
// Create a new User object and save it to the database
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password), // Hash the password for security
'role' => $request->role
]);
// Generate an API token for the newly registered user
return response()->json([
'token' => $user->createToken('auth-token')->plainTextToken,
'message' => 'User registered successfully'
], 201); // 201 Created status code
}
public function login(Request $request)
{
// Validate the email and password provided in the request
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required|string'
]);
if ($validator->fails()) {
return response()->json(['error' => $validator->errors()], 422); // Return validation errors
}
// Find the user by their email address
$user = User::where('email', $request->email)->first();
// Check if a user was found and the password matches
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['error' => 'Invalid credentials'], 401); // 401 Unauthorized status
}
// Create an API token for the user
return response()->json([
'token' => $user->createToken('auth-token')->plainTextToken,
'message' => 'User logged in successfully'
]);
}
public function logout(Request $request)
{
// Delete the current access token for the authenticated user
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'User logged out'], 200); // 200 Success status code
}
}
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['error' => 'Invalid credentials'], 401); // 401 Unauthorized status
}
This part of the code means
- “If there’s no user OR if the password check fails…”.
- !Hash::check($request->password, $user->password): Here, the ! negates the result of the Hash::check function. Hash::check compares the plain text password provided in the request with the hashed password stored in the database. If they don’t match, it returns false, and the ! turns that into true
- Authorization
- Generating the Middleware
Use Laravel’s artisan command to generate the middleware:
php artisan make:middleware CheckRole
This will create a file named CheckRole.php inside your app/Http/Middleware directory.
- Implementing the Middleware Logic
- Open the newly created CheckRole.php file.
public function handle(Request $request, Closure $next, ...$roles)
{
// Check if the authenticated user's role is within the allowed roles
if (!in_array($request->user()->role, $roles)) {
// User's role is not authorized, return an error response
return response()->json(['error' => 'Unauthorized'], 403);
}
// User's role is authorized, proceed to the next middleware or controller
return $next($request);
}
- Registering the Middleware
To make Laravel aware of your middleware, you need to register it.
- Open the app/Http/Kernel.php file.
- Locate the $routeMiddleware array. Add the following line within that array:
'CheckRole' => \App\Http\Middleware\CheckRole::class,
Explanation of Middleware Code
- handle method: This is the core of the middleware. It intercepts the incoming request before it hits your controller.
- …$roles: Using the splat operator (…) allows our middleware to accept multiple roles.
- in_array($request->user()->role, $roles): Checks if the authenticated user’s role exists within the allowed roles.
- return $next($request);: Allows the request to proceed to the controller if authorization is successful.
Finally update your routes/api as follows…
// Authentication routes
Route::prefix('auth')->group(function () {
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); // Protected route
});
// Global authentication for API routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
// Other API routes requiring general authentication go here...
// Admin-only routes
Route::middleware('CheckRole:admin')->group(function() {
// Routes accessible only by admins (e.g., POST /books, PUT /books/{id}, DELETE /books/{id} )
// Bookstore API routes
Route::resource('books', BookController::class)->except(['create', 'edit']);
});
});
Let’s break down the key differences between the auth:sanctum and CheckRole:admin middlewares in your route group:
auth:sanctum
- Type: Authentication Middleware
- Purpose: Acts as a gatekeeper, ensuring that ONLY authenticated users can access any routes within its group.
- How it Works:
- Looks for a valid Sanctum API token (usually in the Authorization header).
- Verifies the token against your user database.
- If no token or an invalid token is provided, it rejects the request with a 401 Unauthorized error.
CheckRole:admin
- Type: Authorization Middleware (custom-made)
- Purpose: Provides finer-grained control on top of authentication, specifically checking if the user has the ‘admin’ role.
- How it Works:
- Assumes the user is already authenticated (this middleware is nested within the auth:sanctum group).
- Retrieves the authenticated user’s ‘role’ property.
- Denies access with a 403 Forbidden error if the user is not an ‘admin’.
In Summary
Middleware | Type | Focus |
auth:sanctum | Authentication | Is there a valid logged-in user? |
CheckRole:admin | Authorization | Does the user have the right level of access? |
Testing Plan:
- Unauthenticated Access (GET /api/books):
- Method: GET
- Headers: None
- Expected Response: 401 Unauthorized
- User Authentication:
- Register a new user (POST /api/auth/register):
- Method: POST
- Body (JSON): { “name”: “Test User”, “email”: “testuser@example.com”, “password”: “testpassword”, “role”: “user” }
- Expected Response: 201 Created with an API token
- Login as the user (POST /api/auth/login):
- Method: POST
- Body (JSON): { “email”: “testuser@example.com”, “password”: “testpassword” }
- Expected Response: 200 OK with an API token
- Standard User Access:
- Access protected route (GET /api/books):
- Method: GET
- Headers:
- Key: Authorization
- Value: Bearer <your_api_token> (from login response)
- Expected Response: 403 Forbidden (not authorized for user)
- Attempt to create a book (POST /api/books):
Creating new book record
{
"title": "The Hitchhiker's Guide to the Galaxy",
"author": "Douglas Adams",
"isbn": "978-0345391803",
"price": 12.99
}
Method: POST
- Headers: Include Authorization header
- Body (JSON): Valid book data
- Expected Response: 403 Forbidden (not authorized for user)
- Admin User Access (Repeat steps 2a and 2b with “role”: “admin” during registration):
{
"name": "Admin User",
"email": "admin@example.com",
"password": "password",
"role": "admin"
}
{
“email”:admin@example.com”,
“password”:”password”
}
- Use admin token to create a book (POST /api/books)
Creating new book record
{
"title": "The Hitchhiker's Guide to the Galaxy",
"author": "Douglas Adams",
"isbn": "978-0345391803",
"price": 12.99
}
- Expected Response: 201 Created (successful creation)
Watch The YouTube Video Here: