How to Build a Rest API with Laravel: A Beginners Guide
Last Updated on May 29, 2024
With the rise of mobile and javascript web frameworks such as React and Vue, Restful APIs have seen their popularity increase. This is because you can maintain one backend serving multiple front-end clients.
Laravel provides a good environment and ecosystem for building your Rest API.
First-party packages such as Laravel Passport and Laravel Sanctum provide API authentication implementation making authentication easy.
Laravel Breeze provides starter templates that can help with reset password features.
Socialite and Scout provide Social Login implementations and Full-text search functionalities.
The laravel ecosystem provides solutions to almost all problems you can encounter in your development journey thus providing maximum productivity to a developer.
This tutorial will explore how to create a Laravel Rest API with authentication using Laravel Sanctum.
What is a Restful API?
REST stands for REpresentational State Transfer and it is a pattern used for communication between applications through HTTP. The protocol is stateless meaning no session is stored between the applications. Each request is therefore processed as if it is a new request even though it is repeated.
A benefit of REST APIs is that they can easily be cached. It is easy to cache a response from the Rest API in a service such as Redis or Memcached and thus easy to scale.
For an API to be considered Restful, it has to have the following
- Must be accessible over a URL or endpoint
- Must use any of the REST Methods
The common REST Methods are:
GET-Fetch resources from an API
POST-Create a resource in the API
PUT/PATCH-Update a resource in the API
DELETE– Delete a resource from an API
- Can have HTTP headers
- Must return a valid response code in each response.
How to build a REST API with Laravel
Create a new Application
The first step is to create a new Laravel application.
laravel new rest
Set up a Model and Migration
The next step is to create a Model and its corresponding migration file. This acts as a blueprint for our database table. In this tutorial, I will use Products as my resource.
php artisan make:model Products -m
The -m flag will instruct Laravel to create the corresponding migration file of the Products Model.
//App/Models/Products
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Products extends Model
{
use HasFactory;
}
The migration file generated will resemble the one below.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('products');
}
};
We can start by updating the migration file by adding more columns to our database. I will add the following columns: product name, product price and product description
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->double('price');
$table->longText('description');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('products');
}
};
We can then update the Products model by registering the mass-assignable variables. This helps prevent SQL injection by instructing laravel only to accept data containing the specified keys/variables.
//App/Models/Products
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Products extends Model
{
use HasFactory;
protected $fillable = [
'name', 'price', 'description'
];
}
The last step is to set up the database credentials in the .env file and create the database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel-rest
DB_USERNAME=root
DB_PASSWORD=password
The final step is to migrate the database.
php artisan migrate
Create a Database Seeder and Factory
When developing, I prefer to use fake data to ensure that I develop as fast as possible. Laravel provides a handy Factory facade that can allow us to use Faker to generate dummy data.
We can run this command to create a Factory
php artisan make:factory ProductsFactory
This will create a file in the databases/factories folder
We can update the file as follows
//database/factories/ProductsFactory
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Products>
*/
class ProductsFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->word,
'price' => $this->faker->numberBetween(1, 99),
'description' => $this->faker->sentence()
];
}
}
Now that our factory is ready, we can call it in the DatabaseSeeder file to seed our database.
//database/seeders/DatabaseSeeder
<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
\App\Models\Products::factory(10)->create();
}
}
We can now seed the database
php artisan db:seed
Create a Controller
Let’s now create a Controller which will contain all the Business logic for the API.
php artisan make:controller ProductsController -r
The -r flag will generate a Controller that is resourceful. This means it will create a controller with all the required methods for a Restful API.
The main methods we will use are index, show, store, update and destroy. We can delete the create and edit methods as we will not need them. We can update the Products Controller as shown below
//App/Http/Controllers/ProductsController
<?php
namespace App\Http\Controllers;
use App\Http\Resources\ProductResource;
use App\Models\Products;
use Illuminate\Http\Request;
class ProductsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param Products $product
* @return \Illuminate\Http\Response
*/
public function show(Products $product)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param Products $product
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Products $product)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param Products $product
* @return \Illuminate\Http\Response
*/
public function destroy(Products $product)
{
//
}
}
These methods can be mapped to the default HTTP verbs (get, post, patch/put and delete).
Index(Get all Products)
We can use this method to return all products present in the database
use App\Models\Products;
public function index()
{
return Products::all();
}
We can further customise it by paginating it or caching the response from the database.
Show(Get a Single Product)
We can use this method to return a single product present in the database
We can pass the product id as a parameter and we can then fetch it from the database
Note: I am using API Resources which I will discuss later in the article.
use App\Http\Resources\ProductResource;
use App\Models\Products;
public function show(Products $product)
{
return new ProductResource($product);
}
Store(Create a Product)
We can create a new product record in the database using this method. A post method can be made to this method through an endpoint to create the record
use App\Http\Resources\ProductResource;
use App\Models\Products;
public function store(Request $request)
{
$product_name = $request->input('name');
$product_price = $request->input('price');
$product_description = $request->input('description');
$product = Products::create([
'name' => $product_name,
'price' => $product_price,
'description' => $product_description,
]);
return response()->json([
'data' => new ProductResource($product)
], 201);
}
Update(Update Product Details)
To update the product details, we can update the logic of the update method
use App\Http\Resources\ProductResource;
use App\Models\Products;
public function update(Request $request, Products $product)
{
$product_name = $request->input('name');
$product_price = $request->input('price');
$product_description = $request->input('description');
$product->update([
'name' => $product_name,
'price' => $product_price,
'description' => $product_description,
]);
return response()->json([
'data' => new ProductResource($product)
], 200);
}
Destroy(Delete a Product)
At times you might want to delete products from the database. We can add the following code to delete products from the database.
use App\Models\Products;
public function destroy(Products $product)
{
$product->delete();
return response()->json(null,204);
}
Routes and endpoints
Let’s now create the endpoints that will be accessible over HTTP. We can add the routes in the routes/api.php file
//routes/api.php
<?php
use App\Http\Controllers\ProductsController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::get('products', [ProductsController::class, 'index'])->name('products.index');
Route::get('products/{product}', [ProductsController::class, 'show'])->name('products.show');
Route::post('products', [ProductsController::class, 'store'])->name('products.store');
Route::put('products/{product}', [ProductsController::class, 'update'])->name('products.update');
Route::delete('products/{product}', [ProductsController::class, 'destroy'])->name('products.destroy');
These endpoints are mapped to the methods in the ProductsController we created earlier.
We can now test the index method through a GET request. This will return the following response
[
{
"id": 1,
"name": "quo",
"price": 15,
"description": "Ut rerum aut deleniti eveniet ad et ullam perferendis.",
"created_at": "2022-11-18T15:18:13.000000Z",
"updated_at": "2022-11-18T15:18:13.000000Z"
},
{
"id": 2,
"name": "maxime",
"price": 70,
"description": "Natus officiis repellat vero ea voluptatem mollitia similique.",
"created_at": "2022-11-18T15:18:13.000000Z",
"updated_at": "2022-11-18T15:18:13.000000Z"
}
]
Formatting the Response
The response above is returned in JSON format. It includes details from the database with the column names as the keys.
What if you want to format this response? One thing you might not want to expose is the created_at and updated_at data. You might also want to calculate and return a predefined discount back as a response.
Laravel allows us to customize our responses using API resources.
In this example, I will assume that all products have a 10% discount. I will therefore return the product price and the discounted price as part of the payload in the response.
php artisan make:resource ProductResource
This will transform a model into an array.
//App/Http/Resources/ProductResource
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'product_name' => $this->name,
'product_price' => "$" . $this->price,
'discounted_price' => "$" . ($this->price * 0.9),
'discount' => "$" . ($this->price * 0.1),
'product_description' => $this->description,
];
}
}
Let’s update our index method in Products Controller to use the Product resource
public function index()
{
return ProductResource::collection(Products::all());
}
This will return the newly formatted response
{
"data": [
{
"id": 1,
"product_name": "quo",
"product_price": "$15",
"discounted_price": "$13.5",
"discount": "$1.5",
"product_description": "Ut rerum aut deleniti eveniet ad et ullam perferendis."
},
{
"id": 2,
"product_name": "maxime",
"product_price": "$70",
"discounted_price": "$63",
"discount": "$7",
"product_description": "Natus officiis repellat vero ea voluptatem mollitia similique."
}
]
}
Response Codes
It is important to always return a valid response code in each request. These response codes can be used by a consuming application or client to know what exactly happened on the server side.
Here is a list of the most common response codes
- 200 –Ok. This signifies it is a success code and it is the default response code
- 201-Created. This shows that a resource has been created. It is useful for POST requests.
- 204-No Content. This signifies that the action was successful but no content is returned. It is useful for DELETE requests since a deleted resource cannot return any body content
- 400-Bad Request. This signifies that the client passed an invalid request to the server
- 401-Unauthorized. This signifies that the client is not authorized to access the resource. It is typically used in authentication and authorization services.
- 403-Forbidden. This signifies that a user is authenticated but is not allowed to access the resource.
- 404-Not Found. This signifies that a resource is not found
- 500-Internal Server Error. This implies that there is an error at the server level
Laravel allows us to specify the correct response code using the response()->json() helper function. It is important to always return a response code so that the client/frontend can display the correct data and “fail gracefully” in the case of an error.
response()->json(data,status code)
Setting up Authentication with Laravel Sanctum
We have discussed the most basic way of creating a CRUD REST API in Laravel. We now want to add authentication to our API in order to secure it. There are two ways in which we can implement authentication; either through Laravel Passport or Laravel Sanctum.
Passport provides a way in which applications authenticate themselves over the internet. It basically adds Oauth implementation to your API which other systems can use to authenticate themselves when interacting with your API. It is useful for public APIs where you might want to track API usage and also limit requests for each API key.
Sanctum on the other hand provides a stateless integration of the normal authentication service by using email and passwords to provide authentication to a client. It is useful for private/internal APIs that don’t need all the features provided by an OAuth Server.
In this part, I will use Laravel Sanctum to create a simple authentication service for my API. I will use the default email and password to authenticate a user.
To learn more about the two authentication packages, you can read the Laravel Sanctum article or the Laravel Passport article.
Configuration and Setup
We will first create an authentication scaffold using Laravel Breeze
composer require laravel/breeze --dev
We can then install the package
php artisan breeze:install
php artisan migrate
npm install
npm run dev
This will create a basic authentication scaffold and a forgot and reset password functionality out of the box.
Let’s now install Laravel sanctum and set it up
composer require laravel/sanctum
Next is to publish Sanctum’s configurations
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Finally, we can migrate the database that will store the access tokens
php artisan migrate
The last step is to allow Sanctum to issue access tokens which will be used to authenticate users’ requests.
App/Models/User
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; //import the trait
class User extends Authenticatable
{
use HasApiTokens; //add this trait
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
We can now create a new controller that will be responsible for the Authentication
php artisan make:controller UserAuthenticationController
Register
We can start by fleshing out the user registration logic. We will use the default user table that comes with laravel but feel free to add and remove some columns as you please
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
public function register(Request $request)
{
$name = $request->input('name');
$email = strtolower($request->input('email'));
$password = $request->input('password');
$user = User::create([
'name' => $name,
'email' => $email,
'password' => Hash::make($password)
]);
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'message' => 'User Account Created Successfully',
'access_token' => $token,
'token_type' => 'Bearer',
], 201);
}
From the code, we can see that a user account is created in the database and an access token is issued to the user and is returned back to the user as a response.
Login
The next important feature is to allow users to log in. We can add the following logic
use Illuminate\Support\Facades\Auth;
public function login(Request $request)
{
$email = strtolower($request->input('email'));
$password = $request->input('password');
$credentials = [
'email' => $email,
'password' => $password
];
if (!Auth::attempt($credentials)) {
return response()->json([
'message' => 'Invalid login credentials'
], 401);
}
$user = User::where('email', $request['email'])->firstOrFail();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
],200);
}
Similar to the registration, we need to return the access token as soon as the user credentials have been validated.
If the credentials are wrong, we need to alert the user that their credentials are wrong. But how?🤔
Since a REST API is stateless, there is no way of flashing responses in real-time. We, therefore, need to return a valid response code and a descriptive message so that a user knows what happened at the server level.
We can use a simple if else statement but this might make our code less clean. A solution is to use the default Exception Handler provided by Laravel.
We can add some logic to apply to the whole API and catch any Authentication Exceptions and return back a valid API Response back to the client.
We can update the App/Exceptions/Handler.php to resemble the one below
App/Exceptions/Handler
<?php
namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Request;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->renderable(function (AuthenticationException $exception, Request $request) {
if ($request->is('api/*')) {
if ($exception instanceof AuthenticationException) {
return $request->expectsJson() ?:
response()->json([
'message' => 'Unauthenticated.',
'status' => 401,
'Description' => 'Missing or Invalid Access Token'
], 401);
}
}
});
}
}
This will come in handy when we discuss middleware.
Logout
Of course in each system, we need a logout functionality so we will also create its function
public function logout()
{
auth()->user()->tokens()->delete();
return response()->json([
'message' => 'Succesfully Logged out'
], 200);
}
Note that we are using the delete function to invalidate an existing access token and delete it from the database. This makes the access token unusable and we can comfortably say the user is logged out.
The final piece left is to create the endpoints/routes in the routes/api.php file.
Route::post('login', [UserAuthenticationController::class, 'login']);
Route::post('register', [UserAuthenticationController::class, 'register']);
Route::post('logout', [UserAuthenticationController::class, 'logout'])->middleware('auth:sanctum');
Only the logout endpoint needs middleware because it requires a token to be passed to verify which user is being logged out of the system.
Scopes and Middlewares
Scopes are commonly used in APIs to assign abilities to tokens. They help flesh out what a user can and cannot do in a system. Think of them as permissions you grant to users once you onboard them. Some users can access financial records in a system while others cannot.
Middlewares on the other hand are used to protect endpoints from unauthorized access. Think of them as a gate that filters out valid requests from unauthorized and malicious requests. They help safeguard your endpoints from being accessed by users who have no role in accessing them.
We can add the auth:sanctum middleware to the product’s endpoints to safeguard them. This means that for a request to be considered valid, it has to have the Authorization: Bearer token headers(where the token is the actual access token issued by the API).
HEADERS {
'Content-Type': 'application/json',
'Authorization': 'Bearer <Token>'
}
Each request has to include the bearer token in the headers or the server will respond with a 401(Unauthorized) response.
Grouping Endpoints
In most cases, you might have multiple endpoints that share some common things such as version(e.g v1,v2), prefixes(admin), or middleware.
It is important to always version your APIs especially if they are being used by the public. This helps in preventing breaking changes and also allows for backward compatibility in the event new APIs are available.
We can group these endpoints using the Route::group method
Route::group(['middleware' => ['auth:sanctum']], function () {
//All Routes that share the auth:sanctum middleware
});
This way we can have our code organized and clean. In this example, I will group the routes to be of version 1 and use the auth:sanctum middleware.
We might also want to reduce the size of the api.php file by making each resource use apiresource. This simple function reduces the file size by almost 60% and makes reading and maintaining the code an easy task.
Route::apiResource('products', ProductsController::class);
The apiresource method under the hood works by mapping the main functions(index, show, store, update and delete) in our controller to their various endpoints.
For example products.show is mapped to the endpoint api/products/{product}.
These are some cool techniques you can use to optimize your development experience and become more productive when developing your APIs.
The final routes/api.php file should resemble the one below
//routes/api.php
<?php
use App\Http\Controllers\ProductsController;
use App\Http\Controllers\UserAuthenticationController;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
//These routes are NOT protected using middleware
Route::prefix('v1')->group(function () {
Route::post('login', [UserAuthenticationController::class, 'login']);
Route::post('register', [UserAuthenticationController::class, 'register']);
});
//These routes are protected using middleware
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
Route::post('logout', [UserAuthenticationController::class, 'logout']);
Route::apiResource('products', ProductsController::class);
});
Testing using Postman
The last step in this tutorial is to test our API through an HTTP Client. There are numerous HTTP Clients such as Postman and Thunderclient just to name a few.
I personally use Thunderclient because it integrates seamlessly with Vscode which is my goto editor. It can be added as a Vscode extension thereby making your Vscode editor a powerful editor and REST API client at the same time.
It improves my productivity and development experience immensely. You are free to use any other HTTP Client of your choice.
I will use postman in this article as most people are familiar with it.
We will break down the test from authentication to the basic CRUD
Authentication
We can further break down authentication into registration, login and logout.
Registration
We can make a POST request to the endpoint api/v1/register with the body on postman
Login
To test login functionality, we can make a POST request to the api/v1/login
A failed login attempt returns a 401(Unauthorized) Status Code and also returns a descriptive message to the client
A successful login attempt returns a 200(OK) Status Code and also returns an access token that can be stored for future use.
Logout
Logout can be tested by making a POST request to the api/v1/logout endpoint. We also need to attach the bearer token assigned to us during registration/login to the Headers.
Upon success, a descriptive message and a 200(OK) Status Code are returned back to a User.
We can now test the Product resource. Similar to log out, we also need to attach the bearer token assigned to us during registration/login to the Headers. This is because all the product’s endpoints are protected by the auth:sanctum middleware.
If we try to access any product’s endpoint without the access token, the server will respond with a 401(Unauthorized) Status Code.
This is because we registered an Exception Handler to catch any access to the API that is not authorized and return a 401 Status Code with a descriptive response. This protects our API endpoints from unauthorized access.
Get All Products
All products can be returned by making a GET request to the api/v1/products endpoint.
Get A Single Product
We can fetch a single product from the API by making a GET request to the api/v1/products/{id} endpoint
Add A Product
We can add a product by making a POST request to the api/v1/products endpoint
Update A Product
To update a product’s details, we can make a PUT/PATCH request to the api/v1/products/{id} endpoint.
Delete A Product
We can make a DELETE request to the api/v1/products/{id} endpoint to delete a product from the database
In Closing
In this article, we have covered what Rest APIs are and how to create one in Laravel. We have also covered API authentication and Using Middleware to control access to your API. I hope this article was insightful and helped you create a powerful REST API in laravel.
In the next article, I will cover how to generate/create documentation for this REST API.
Thank you for reading.
Further Reading Suggestion: Guide to Hiring API Developers.
Truly insightful !! Thank you
Glad you liked it.
It might be something else in my setup interfering but when I was testing the logout it wasn’t working.
Using Auth::attempt() to validate the login details was also logging them in with the Session, so the bearer token wasn’t even needed. Changed it to Auth::validate() and the problem was solved.
It’s a very thorough tutorial and easy to follow so thank you.
Glad you managed to solve some of the issues you were experiencing.