Laravel: Creating Restful API for Your Website

Laravel: Creating Restful API for Your Website

Introduction

Today a lot of websites use front-end JavaScript frameworks like React.js or Vue.js,
which require the backend to have some Restful API. Restful API is needed for mobile
apps, which consume backend APIs to do actions on behalf of a user. Restful APIs are also used
when you need to provide some integration methods for third-party systems. In this article,
we will show how to create a simple Restful API in Laravel. We will also create an authentication system
which will provide API access for SPAs with cookies, and for mobile apps and third-party systems
by issuing API tokens.

Create the Product Model

We will use the products and categories database as a simple example. A category may contain many products. A product belongs to one category.

First of all, we will create the categories
and products database tables. A Category will have the id and name fields. A Product will have the id, name, sku,
price, and category_id fields. Create a Laravel migration file named database/migrations/2021_02_03_123000_create_two_tables.php:


With each product you can do the following:
- GET `/api/products` - get the list of existing products
- GET `/api/products/{id}` - get a product by ID
- POST `/api/products` - create a new product
- PUT  `/api/products/{id}` - update a product by ID
- DELETE `/api/products/{id}` - delete a product by ID

Laravel provides a convenient way to create Restful APIs via resourceful controllers.

Create a route for the resourceful controller in routes/api.php:

Route::resource('products', 'ApiController');

This route defines the GET, POST, PUT and DELETE actions for our resourceful controller.

Then create a file app/Http/Controllers/ApiController.php and put the following stub code in it:


response like below, assuming you have inserted some categories into your categories table
and some products into your products table:
{
    "status": "SUCCESS",
    "products": [
        {
            "id": 5,
            "name": "Bottle of Mineral Water",
            "sku": "BW-12345",
            "price": "10.95",
            "category_id": 3
        },
        {
            "id": 7,
            "name": "T-Shirt",
            "sku": "TS-1532",
            "price": "25.90",
            "category_id": 2
        },
        {
            "id": 8,
            "name": "Hydraulic Drill",
            "sku": "HD-94134",
            "price": "200.50",
            "category_id": 1
        }
    ]
}

In reality, there can be a lot of products (thousands or even millions), and the JSON response containing the list of all products
can become VERY long. To handle this problem, we can implement pagination. Modify the index() action as following:

public function index(Request $request)
{
    $page = $request->query('page', 0);
    $limit =  $request->query('limit', 10);
    $products = Product::skip($page*$limit)->take($limit)->get();

    $jsonData = ['status' => 'SUCCESS', 'products' => []];

    foreach ($products as $product){

        $jsonData ['products'][] = [
            'id' => $product->id,
            'name' => $product->name,
            'sku' => $product->sku,
            'price' => $product->price,
            'category_id' => $product->category->id,
        ];
    }

    return response()->json($jsonData);
}

Now you can enter the URL http://localhost/api/products?page=1&limit=10 in your web browser to see only a portion of products.
To show different pages of products, just increment the page query parameter. The query parameter limit defines how many
products per page the API will return.

Retrieving the Given Product

The action show() allows to retrieve an existing Product by its ID:

public function show($id)
{
    $product = Product::where('id', $id)->firstOrFail();

    $jsonData = [
        'status' => 'SUCCESS',
        'product' => [
            'id' => $product->id,
            'name' => $product->name,
            'sku' => $product->sku,
            'price' => $product->price,
            'category_id' => $product->category->id,
        ]
    ];

    return response()->json($jsonData);
}

Now if you open the URL http://localhost/api/products/5 in your browser (replace 5 with the actual product ID), you should see a response like below:

{
    "status": "SUCCESS",
    "product": {
        "id": 5,
        "name": "Bottle of Mineral Water",
        "sku": "BW-12345",
        "price": "10.95",
        "category_id": 3
    }
}

Creating a Product

The action store() allows to create a product:

public function store(Request $request)
{
    $data = $request->all();               
    $product = new Product($data);
    $product->save();

    return [
            'status' => 'SUCCESS',
            'product' => [
                'id' => $product->id,
                'name' => $product->name,
                'sku' => $product->sku,
                'price' => $product->price,
                'category_id' => $product->category->id,
            ],
        ];
}

To create a real product, we will need to use the CURL command line utility, since it allows to make POST requests:

curl --location --request POST 'http://localhost/api/products' 
--header 'Content-Type: application/json' 
--data-raw '{"name":"Big Bananas", "sku":"BA-1495", "price":"5.95", "category_id":3}'

The API should response like below:

{
    "status": "SUCCESS",
    "product": {
        "id": 23,
        "name": "Big Bananas",
        "sku": "BA-1495",
        "price": "5.95",
        "category_id": 3
    }
}

Since users of your API can supply any data they want (including incorrect data), we need to perform validation
to ensure the data passes some rules. For example, the name field should be a string shorter than 512 characters in length,
and it should always present (be required). Modify the store() method as follows to add validation:

public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|unique:products|max:512',
        'sku' => 'required|unique:products|max:128',
        'price' => 'required|numeric|min:0|max:99999',
        'category_id' => 'required|exists:categories,id',
    ]);

    $data = $request->all();               
    $product = new Product($data);
    $product->save();

    return [
        'status' => 'SUCCESS',
        'product' => [
            'id' => $product->id,
            'name' => $product->name,
            'sku' => $product->sku,
            'price' => $product->price,
            'category_id' => $product->category->id,
        ],
    ];
}

Now if you try to supply some invalid data, you will get a response describing what was wrong
with your input.

Updating an Existing Product

The action update() allows to update an existing product:

public function update(Request $request, $id)
{
    $validated = $request->validate([
        'name' => 'unique:products|max:512',
        'sku' => 'unique:products|max:128',
        'price' => 'numeric|min:0|max:99999',
        'category_id' => 'exists:categories,id',
    ]);

    $data = $request->all();
    $product = Product::where('id', $id)->firstOrFail();
    $product->fill($data);
    $product->save();

    $jsonData = [
        'status' => 'SUCCESS',
        'product' => [
            'id' => $product->id,
            'name' => $product->name,
            'sku' => $product->sku,
            'price' => $product->price,
            'category_id' => $product->category->id,
        ]
    ];

    return response()->json($jsonData);
}

To update a real product, make the following PUT request with CURL:

curl --location --request PUT 'http://localhost/api/products/5' 
--header 'Content-Type: application/json' 
--data-raw '{"price":10.1}'

The API should return something like below:

{
    "status": "SUCCESS",
    "product": {
        "id": 5,
        "name": "Bottle of Mineral Water",
        "sku": "BW-12345",
        "price": 10.1,
        "category_id": 3
    }
}

Deleting a Product

The action destroy() allows to delete an existing product:

public function destroy($id)
{
    $product = Product::where('id', $id)->firstOrFail();
    $product->delete();

    $jsonData = [
        'status' => 'SUCCESS',
    ];

    return response()->json($jsonData);
}

To delete some product, issue the following DELETE request with CURL:

curl --location --request DELETE 'http://localhost/api/products/5' 

The API should response:

{
    "status": "SUCCESS"
}

Forcing JSON Response for the API Route

Restful API should alway return JSON response, even if there was some PHP exception in your code.
To force JSON responses on every action, we need to use a Middleware. To do that,
create the file app/Http/Middleware/ForceJsonResponse.php:


describing the exception and giving the stack trace. However, this may be undesirable since
we want to give the user only the minimum of information. To do that,
modify the render() method in the app/Exceptions/Handler.php file:
public function render($request, Exception $exception)
{
    if ($request->is('api/*')) {

        if ($exception instanceOf IlluminateValidationValidationException) {
            $messages = [];
            foreach ($exception->errors() as $key => $errors) {
                $messages[] = implode(', ', $errors);
            }

            $message = implode(', ', $messages);

        } else {
            $message = $exception->getMessage();
        }

        $jsonData = [
            'status' => 'ERROR',
            'message' => $message,
        ];

        return response()->json($jsonData);
    }

    return parent::render($request, $exception);
}

We basicly did the following. If an URL of some request starts with api, which means some API call,
we return the status ERROR and the message of exception. In addition, for validation exceptions,
we concatenate the list of validation errors. As a result, the user will see a response like below:

{
    "status": "ERROR",
    "message": "Some short and clear error message"
}

Rate Limiting

Rate limiting is very useful if you want to protect your server from being overloaded by
too many API requests. Laravel provides a useful middleware for that named throttle.
Just modify the routes/api.php file as follows (add throttle:20,1 to the list of middleware):

Route::group(['middleware' => ['json.response', 'throttle:20,1']], function () { 

    Route::resource('products', 'ApiController');

});

The line throttle:20,1 means that a particular user will be able to make maximum of 20 requests per minute. In case of
too many requests, the user will see the following response:

{
    "status": "ERROR",
    "message": "Too Many Attempts."
}

Authentication

For now any person can use our API, which is not secure. We want to close the API to everybody except users which we granted access to.
This is also called authentication. In Laravel, you can implement several types of authentication:

  • API token access. It is good for simple APIs where you issue personal access tokens.
  • Cookie based access. It is very simple and suitable for Single Page Apps (SPAs), like React.js or Vue.js frontend apps.
  • OAuth, which is the most secure and good for giving access to your API for third-party systems, although it is the most sophisticated of the three.

In this tutorial, we will implement the API based token access and Cookie based access. In Laravel, we have a module called Sanctum, which allows to do that.

Install Laravel Sanctum with Composer:

composer require laravel/sanctum

Then you need to publish the Sanctum configuration and migration files:

php artisan vendor:publish --provider="LaravelSanctumSanctumServiceProvider"

Run the following command to create the tables for API tokens:

php artisan migrate

Add Sanctum’s middleware to your api middleware group inside of app/Http/Kernel.php file:

'api' => [
    LaravelSanctumHttpMiddlewareEnsureFrontendRequestsAreStateful::class,
    ...
],

API Token Authentication

Modify the app/User.php file and add the HasApiTokens trait to your User model class:

use LaravelSanctumHasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

To protect your API routes, modify routes/api.php as follows (note the auth:sanctum item in the list of middleware):

Route::group(['middleware' => ['json.response', 'auth:sanctum', 'throttle:20,1']], function () { 

    Route::resource('products', 'ApiController');

});

Testing a Token

To issue a test token, you can create a simple POST action in your routes/web.php:

use IlluminateHttpRequest;

Route::get('/tokens/create', function (Request $request) {
    $token = $request->user()->createToken($request->token_name);

    return ['token' => $token->plainTextToken];
});

Now log in to your website with some user (we assume that you already have an authentication system in place),
and type http://localhost/tokens/create?token_name=test. You should see your token returned:

{"token":"2|lFv0wxshfcCGtFbi8xNqPFAacxWjI9ymRe5zqtHw"}

Next, you need to add the Authorization: Bearer header to all your API requests, like below:

curl --location --request GET 'http:/localhost/api/products/3333' 
--header 'Authorization: Bearer 2|lFv0wxshfcCGtFbi8xNqPFAacxWjI9ymRe5zqtHw' 

Cookie Based Authentication

To authenticate SPAs, Laravel Sanctum provides an additional cookie based method of authentication.

First, configure which domains your frontend app will be making requests from. If your SPA is hosted at the same server
where your Laravel backend is hosted, use ‘localhost’. Modify the stateful configuration option in your config/sanctum.php
configuration file to make it look like below:

'stateful' => explode(',', env(
        'SANCTUM_STATEFUL_DOMAINS',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1'
    )),

Next, add the EnsureFrontendRequestsAreStateful middleware to your app/Http/Kernel.php file:

use LaravelSanctumHttpMiddlewareEnsureFrontendRequestsAreStateful;

'api' => [
    EnsureFrontendRequestsAreStateful::class,
    //...
],

Testing Cookie-Based Access

To authenticate your frontend app, first issue a request to the http://localhost/sanctum/csrf-cookie URL
to initialize CSRF protection for the application:

axios.get('/sanctum/csrf-cookie').then(response => {
    // Do login...
});

Once you made that request, Laravel will set an XSRF-TOKEN cookie containing the CSRF token.
You should pass the token to further API requests an X-XSRF-TOKEN header, which is typically
done with your JavaScript HTTP client library automatically.

After getting the CSRF cookie, log in as some user via your /login action (we assume you have your login route in place already).
On successful login, you will be authenticated and should be able to issue API requests without problems.

Conclusion

In this article, we considered how to create a fully-functional Restful API in your Laravel website. The API we created
supports CRUD (Create-Read-Update-Delete) actions for the Product resource. We showed how to implement JSON responses,
error reporting and rate limiting. Finally, we implemented the API token-based and Cookie-based authentication with Laravel Sanctum, so only users who are granted access could
issue the API calls.

Are you looking for Tech Jobs? Discover more on Meritocracy.is!

Leave a Reply

Your email address will not be published.