Learning Management Platform for Written Tutorial Series.

Laravel 6 REST API

Laravel 6 REST API

Almost all successful internet-based companies have APIs. API is an acronym for Application Programming Interface. APIs allows different systems to communicate with one another. Let's say you have developed an android application for our online store. The API can be used to retrieve data from the online store and display it in the mobile application.

The API can also be used to process orders from remote clients such as mobile applications, other websites etc.

Topics to be covered

We will cover the following topics

  • Introduction to REST APIs
  • REST APIs Best Practices
  • Laravel Simple API
  • Laravel API Authentication
  • API Validation
  • Transformers
  • API Error Handling

Introduction to REST APIS

REST is the acronym for Representational State Transition. It is a software architectural design for building scalable web services. REST APIs allow systems to communicate over HTTP using HTTP verbs. HTTP GET is used to get resources, POST used to create new resources, PUT to update existing ones and DELETE to delete existing resources.

REST API BEST Practices

Best practices make developing, working and maintaining APIs easy. The following are some of the best practices that you must follow when working on APIs

Use RESTful URLS

RESTful URLs represent a resource i.e. a customer. A RESTful URLs use nouns to describe resources and use plural nouns not singular ones. For example, the following URL can be considered a RESTful URL

/api/v1/pos/customers

Use HTTP VERBS to determine action to be taken

Its very command when working with traditional URLs to have links like /api/customers/show/1, /api/customers/delete/1 or /api/customers/update/1

When working with RESTful APIs, only a single URL i.e. /api/customers/1 is necessary. The action to be taken is should be determined by the HTTP verb used. For example, /api/customers/1 can be used to get the customer details with the GET verb. Changing the verb to PUT would update the customer resource with id value of 1 wile changing it to DELETE verb would delete the customer record.

Use API Versioning

API evolve as business requirements change. It's common for beginners to create API links like /api/customers. But what happens when requirements change? If you update the endpoint /api/customers then you break all applications that depend on the API. Consider the URL /api/v1/customers. If new requirements need to be incorporated, then you can simply add a new endpoint /api/v2/customers. This way applications that rely on your API are not broken and API consumers can upgrade to the latest version smoothly.

Use query strings to build relations

In today's world, APIs often offer a lot of information and the users might be interested in only small bits of information. Instead of having the following format,

/api/v1/customers/orders/year/2013/lessthan/3/iphones

You can instead use the following format

/api/v1/customers/orders?year=2013&quantity=\<5&product=iphones

This makes it easy for users to query your API with flexibility and you only have to maintain few endpoints.

Partial responses

Bandwidth and time are consumed when users interact with your APIs. If you have an API that has a lot of fields in the response and the user is interested in only a few fields then you can use partial responses and return only the fields that are required. The following URL demonstrates this feature.

/api/v1/customers//1?fields=name,email,address

Response Codes and Error Handling

Every API response should be returned with the appropriate HTTP response code. For example, if a resource has been requested and returned successfully, 200 should be returned as the response code. If the resource is not found, then 404 response code should be returned.

Limit the number of requests in a given time period from the same IP Address

This is for security reasons. If a client computer is compromised, then it can be sending a lot of requests within a very short period of time to crash the server and make the API unavailable for everyone. Limiting the number of requests per given time solves this problem.

Use JSON as the default

Its ok to support various response types such as JSON and XML but these days JSON is the common almost standard format. Unless otherwise, use it as the default response format.

Cache GET results for less frequently changing data

Hitting the database every now and then has performance issues and general affects the user experience. For information that changes less frequently, you can use a cache database such as Redis to improve the performance.

Laravel Simple API

Enough with the theory, let's now look at some practical examples. In this section, we will create a simple inventory API that contains customer details and their orders.

We will use Laravel 6.0.* to create our API.

Create a new project by running the following

composer create-project laravel/laravel larapi 6.0.*

HERE,

  • The above command creates a new Laravel project using the latest version of Laravel version 6.0

Database Models

We will have work with the following models in our API

  • Customer - for customer details
  • Inventory - for the inventory items that customers can order
  • Order - order details master record
  • OrderDetail -- contains the items that have been ordered

Run the following commands to create the above models and their respective database migration files.

php artisan make:model Customer -m
php artisan make:model Inventory -m
php artisan make:model Order -m
php artisan make:model OrderDetail -m

Let's now update the migration files to the following

Open /database/migrations/timestamp_create_customers_table.php

Update the code to the following

<?php

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

class CreateCustomersTable extends Migration
{
    public function up()
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('first_name');
            $table->string('last_name');
            $table->string('email');
            $table->string('physical_address');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    public function down()
    {
        Schema::dropIfExists('customers');
    }
}

Open /database/migrations/timestamp_create_inventories_table.php

<?php

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

class CreateInventoriesTable extends Migration
{
    public function up()
    {
        Schema::create('inventories', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('item');
            $table->string('description');
            $table->integer('quantity_at_hand')->default(777);
            $table->float('price');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    public function down()
    {
        Schema::dropIfExists('inventories');
    }
}

Open /database/migrations/timestamp_create_orders_table.php

<?php

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

class CreateOrdersTable extends Migration
{
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('customer_id');
            $table->timestamp('order_date');
            $table->string('order_notes');
            $table->timestamps();
            $table->softDeletes();

            $table->foreign('customer_id')->references('id')->on('customers');
        });
    }

    public function down()
    {
        Schema::dropIfExists('orders');
    }
}

Open /database/migrations/timestamp\_create_order_details_table.php

<?php

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

class CreateOrderDetailsTable extends Migration
{
    public function up()
    {
        Schema::create('order_details', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('order_id');
            $table->unsignedBigInteger('inventory_id');
            $table->integer('quantity');
            $table->timestamps();
            $table->softDeletes();

            $table->foreign('order_id')->references('id')->on('orders');
            $table->foreign('inventory_id')->references('id')->on('inventories');
        });
    }

    public function down()
    {
        Schema::dropIfExists('order_details');
    }
}

Lets now create our database and configure our application to communicate with the database

Run the following statement in MySQL to create the database

CREATE SCHEMA larapi;

Open .env file

Update the following section to match your database connection parameters

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=larapi
DB_USERNAME=root
DB_PASSWORD=secret

Run the following command to run the migration files

php artisan migrate

Database Seeding

We need to have some data to interact with. We will use faker to generated some dummy customers and inventory records to help us get started.

Run the following commands to create Factories for customers and inventories

php artisan make:factory CustomerFactory --model=Customer
php artisan make:factory InventoryFactory --model=Inventory

Open /database/factories/CustomerFactory.php

Update the code to the following

<?php

use Faker\Generator as Faker;

$factory->define(App\Customer::class, function (Faker $faker) {
    return [
        'first_name' => $faker->firstName,
        'last_name' => $faker->lastName,
        'email' => $faker->email,
        'physical_address' => $faker->address,
    ];
});

Open /database/factories/InventoryFactory.php

Update the code to the following

<?php

use Faker\Generator as Faker;

$factory->define(App\Inventory::class, function (Faker $faker) {
    return [
        'item' => $faker->word,
        'description' => $faker->sentence($nbWords = 6, $variableNbWords = true),
        'quantity_at_hand' => $faker->numberBetween($min = 100, $max = 900),
        'price' => $faker->numberBetween($min = 100, $max = 900),
    ];
});

We will now use tinker to seed the database

Run the following command

php artisan tinker

Once the shell opens, run the following two factory commands

factory(App\Customer::class, 10)->create();

HERE,

  • The above command seeds 10 customer records into our database.

Run the following command for inventories

factory(App\Inventory::class, 30)->create();

HERE,

  • The above command creates 30 inventory records into our database.

Laravel API Resource Routes

A resource route contains all the methods that we need to create, read, update and delete records. Laravel makes it very easy for us to create API resource controllers and routes.

Run the following commands to create the API Resource Controllers

php artisan make:controller API/CustomerController --api
php artisan make:controller API/InventoryController --api
php artisan make:controller API/OrderController --api

HERE,

  • The option --api tells artisan that this is an API resource controller. All the methods that we need will be included.

Let's now create our resource routes

Open /routes/api.php

Add the following lines

Route::group(['prefix' => 'v1','namespace' => 'API'], function(){
    Route::apiResource('customers', 'CustomerController');

    Route::group(['prefix' => 'customers'],function(){
        Route::get('/{id}/orders',[
            'uses' => 'CustomerController@orders',
            'as' => 'customers.orders',
        ]);

        Route::post('/{customer_id}/orders/{order_id}',[
            'uses' => 'CustomerController@order',
            'as' => 'orders.details',
        ]);

        Route::post('/{id}/orders',[
            'uses' => 'CustomerController@order',
            'as' => 'customers.orders',
        ]);
    });
    
    Route::apiResource('inventories', 'InventoryController');
    Route::apiResource('orders', 'OrderController');
});

HERE,

  • route::group(...) groups our routes in the prefix v1. This is a best practice and makes it easy to upgrade the API in the future.
  • Route::apiResource(...) creates the resource routes that contain all the required HTTP verbs, GET,POST,PUT/PATCH and DELETE.

Run the following artisan command to view the registered routes

php artisan route:list

Laravel 6 API Routes

Laravel API Models

In the above section, we only created our models but didn't make any updates to them. In this section, we will update our models to include the following function.

  • Soft deletes -- a soft delete in Laravel does not actually delete the record from the database but only marks it as deleted using a timestamp. For us to do this, we will need to import the soft delete trait and apply it to the model
  • Define relationships -- our tables have relationships. For example, customers is related to orders table using a one-to-many relationship. Orders I related to order details while inventory table is related to order details table.
  • Mass assignment protection -- we will need to whitelist the database fields that can be updated using our models.

Open app/Customer.php

Update the code to the following

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Customer extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'first_name',
        'last_name',
        'email',
        'physical_address',
    ];

    public function orders() {
        return $this->hasMany(Order::class);
    }
}

HERE,

  • use Illuminate\Database\Eloquent\SoftDeletes; Imports the SoftDeletes trait.
  • use SoftDeletes; applies the SoftDeletes trait to the model.
  • protected $fillable = [...] whitelists the mass protected assignment fields.
  • public function orders() {...} defines the orders relationship between the customers and orders tables via the model Orders

Open app/Inventory.php

Update the code to the following

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Inventory extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'item',
        'description',
        'price',
        'quantity_at_hand',
    ];

    public function orders() {
        return $this->hasMany(OrderDetails::class);
    }
}

Open app/Order.php

Update the code to the following

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Order extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'customer_id',
        'order_date',
        'order_notes',
    ];

    public function customer() {
        return $this->belongsTo(Customer::class);
    }

    public function details() {
        return $this->hasMany(OrderDetail::class);
    }
}

Open app/OrderDetail.php

Update the code to the following

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class OrderDetail extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'order_id',
        'inventory_id',
        'quantity',
    ];

    public function order() {
        return $this->belongsTo(Order::class);
    }
}

That's it for our models. Let's now move on to controllers.

Laravel API Controllers

In the above section, we only created the controllers and left the default methods in place. In this tutorial, we will update the boiler plate code methods.

Open app/Http/Controllers/API/CustomerController.php

Update the code to the following

<?php

namespace App\Http\Controllers\API;

use Carbon\Carbon;
use App\Customer;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class CustomerController extends Controller
{
    public function index()
    {
        $customers = Customer::all();

        return response()->json([
            'error' => false,
            'customers'  => $customers,
        ], 200);
    }

    public function store(Request $request)
    {
        $customer = Customer::create($request->all());

        return response()->json([
            'error' => false,
            'customer'  => $customer,
        ], 200);
    }

    public function show($id)
    {
        $customer = Customer::with('orders')
            ->with('orders.details')
            ->find($id);
        
        return response()->json([
            'error' => false,
            'customer'  => $customer,
        ], 200);
    }

    public function update(Request $request, $id)
    {
        $customer = Customer::find($id);

        $customer->first_name = $request->input('first_name');
        $customer->last_name = $request->input('last_name');
        $customer->email = $request->input('email');
        $customer->physical_address = $request->input('physical_address');

        $customer->save();
        
        return response()->json([
            'error' => false,
            'customer'  => $customer,
        ], 200);
    }

    public function destroy($id)
    {
        $customer = Customer::find($id);
        $customer->delete();

        return response()->json([
            'error' => false,
            'message'  => "The customer with the id $customer->id has successfully been deleted.",
        ], 200);
    }

    public function order(Request $request, $id){
        $customer = Customer::find($id);

        $order = $customer->orders()->create([
            'order_date' => Carbon::now(),
            'order_notes' => $request->input('order_notes'),
        ]);
        
        $items = $request->input('items');

        foreach($items as $item){
            $order->details()->create([
                'inventory_id' => $item['inventory_id'],
                'quantity' => $item['quantity'],
            ]);
        }
        
        return response()->json([
            'error' => false,
            'order'  => $order,
        ], 200);
    }
}

HERE,

  • In the above code, we perform create, read, update and delete operations on the models. We are also performing data validation and returning appropriate errors in JSON format.

Open /app/Http/Controllers/InventoryController.php

Update the code to the following

<?php

namespace App\Http\Controllers\API;

use App\Inventory;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class InventoryController extends Controller
{
    public function index()
    {
        $inventories = Inventory::all();

        return response()->json([
            'error' => false,
            'inventories'  => $inventories,
        ], 200);
    }

    public function store(Request $request)
    {
        $inventory = Inventory::create($request->all());

        return response()->json([
            'error' => false,
            'inventory'  => $inventory,
        ], 200);
    }

    public function show($id)
    {
        $inventory = Inventory::find($id);
        
        return response()->json([
            'error' => false,
            'inventory'  => $inventory,
        ], 200);
    }

    public function update(Request $request, $id)
    {
        $inventory = Inventory::find($id);

        $inventory->item = $request->input('item');
        $inventory->description = $request->input('description');
        $inventory->quantity_at_hand = $request->input('quantity_at_hand');
        $inventory->price = $request->input('price');

        $inventory->save();
        
        return response()->json([
            'error' => false,
            'inventory'  => $inventory,
        ], 200);
    }

    public function destroy($id)
    {
        $inventory = Inventory::find($id);
        $inventory->delete();

        return response()->json([
            'error' => false,
            'message'  => "The Inventory with the id $inventory->id has successfully been deleted.",
        ], 200);
    }
}

Open app/Http/Controllers/OrderController.php

Update the code to the following

<?php

namespace App\Http\Controllers\API;

use App\Order;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class OrderController extends Controller
{
    public function index()
    {
        $orders = Order::all();

        return response()->json([
            'error' => false,
            'orders'  => $orders,
        ], 200);
    }

    public function store(Request $request)
    {
        $inventory = Order::create($request->all());

        return response()->json([
            'error' => false,
            'order'  => $order,
        ], 200);
    }

    public function show($id)
    {
        $order = Order::find($id);
        
        return response()->json([
            'error' => false,
            'order'  => $orders,
        ], 200);
    }

    public function update(Request $request, $id)
    {
        $order = Order::find($id);

        $order->order_notes = $request->input('order_notes');

        $order->save();
        
        return response()->json([
            'error' => false,
            'order'  => $order,
        ], 200);
    }

    public function destroy($id)
    {
        $orders = Order::find($id);
        $orders->delete();

        return response()->json([
            'error' => false,
            'message'  => "The Order with the id $orders->id has successfully been deleted.",
        ], 200);
    }
}

Interacting with our API from Postman

We will use Postman to interact with our API. You can use any program that you are comfortable with to interact with the API.

Run the following command to start the built-in port

php artisan serve --port=777

Load the following URL in Postman

http://localhost:777/api/v1/customers/3

You should be able to see results similar to the following

Postman Laravel API

Let's now create a new customer using the API

Load the following URL in Postman

http://localhost:777/api/v1/customers

Fill in the Accept and Content-Type headers to application/json as shown in the image below

Postman Headers

Now, click on the Body tab of Postman and enter the following JSON data

{
	"first_name":"Julius",
	"last_name":"Smith",
	"email":"mrsmith@example.com",
	"physical_address":"New York, US"
}

Postman POST API Request

Click on Send button

A new customer will be created with those details submitted above

If you wish to create a new customer order, then you can use the following JSON data.

{
	"order_notes":"I need the order ASAP",
	"items":[
		{
			"inventory_id":3,
			"quantity":6
		},
		{
			"inventory_id":7,
			"quantity":3
		},
		{
			"inventory_id":13,
			"quantity":2
		}
	]
}

HERE,

  • The above json data defines a field order_notes then an array field that contains item collections.

The URL for creating orders is as follows

http://localhost:777/api/v1/customers/1/orders

Summary

In this tutorial, we have looked at the important factors to consider when creating API and we have learnt how to build an API from scratch using Laravel. We also looked at how to interact with our API to perform various tasks.

What's next?

In the next tutorial, we will work with the same API and look at how we can add user authentication to our API and take advantage of transformers to format the response that we are sending back to the clients.

Let us know what you think of the tutorial in the comments section down below.


...