Creating an Interface for API in Laravel

When writing APIs we sometimes fall into the trap of fetching data from the database and sending them as responses to a request without properly formatting them. Such responses end up having the column names of the tables of the database from which they were fetched. This is a bad practice. To understand clearly what this is, take a look a look at the table below

database-table.png Responding to a Http request to fetch all users would most likely result in this response

{
    "users": [
        {
            "id": 1,
            "first_name": "Jamie Schiller",
            "last_name": "Estella Huels",
            "date_of_birth": "1974-12-05",
            "email": "delilah64@example.com",
            "email_verified_at": "2020-09-13T19:27:33.000000Z",
            "created_at": "2020-09-13T19:27:33.000000Z",
            "updated_at": "2020-09-13T19:27:33.000000Z"
        },
        {
            "id": 2,
            "first_name": "Stanley Bradtke",
            "last_name": "Lucie Miller I",
            "date_of_birth": "2007-04-20",
            "email": "emiliano17@example.org",
            "email_verified_at": "2020-09-13T19:27:33.000000Z",
            "created_at": "2020-09-13T19:27:34.000000Z",
            "updated_at": "2020-09-13T19:27:34.000000Z"
        },
        {
            "id": 3,
            "first_name": "Maxwell Jacobson",
            "last_name": "Kaitlin Steuber",
            "date_of_birth": "1978-08-24",
            "email": "ubaumbach@example.net",
            "email_verified_at": "2020-09-13T19:27:33.000000Z",
            "created_at": "2020-09-13T19:27:34.000000Z",
            "updated_at": "2020-09-13T19:27:34.000000Z"
        }
    ]
}

This is bad practice because,

  1. it exposes our database schema to anyone that has access to the API
  2. a change in column name or database relationship would lead to a change in Http response, which in turn would break the client (whatever consumes our API)
  3. it leads to deep nesting of JSON objects in Http responses.

A solution to this will be to create an interface for our API responses. An interface in this instance will structure our responses by substituting column names with keys that do not expose the database schema and eliminate deep nesting of JSON objects in our responses.

Prerequisite Knowledge

The prerequisite knowledge for this will be

  1. A basic understanding of OOP (Object Oriented Programming) concepts like Abstract classes, Interfaces and Inheritance.
  2. A reasonable knowledge of PHP
  3. Basic knowledge of Laravel. (If you don't know Laravel but you want to learn, I highly recommend Laracasts to beginners).

Objective

The goal here is to create a Staff Management System (Don't be scared, it's going to be very easy). Let us create a new Laravel project.

laravel new staff-mgt-system

For this system the requirement is simple we just want to

  1. view all staff
  2. view all departments
  3. view a single staff with details about his/her department

The Setup

We will be making 3 endpoints for each of the above features. We will be creating 2 models, a Staff model and a Department model. So we'll run the commands as follows

php artisan make:model Staff -mr
php artisan make:model Department -mr

We just created models with migrations and resources (a controller with the default methods available for a controller). The migration file for the Department model looks like this

public function up()
    {
        Schema::create('departments', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('description');
            $table->timestamps();
        });
    }

The migration file for the Staff model looks like this

 public function up()
    {
        Schema::enableForeignKeyConstraints();
        Schema::create('staff', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email');
            $table->string('staff_id');
            $table->timestamp('date_of_birth');
            $table->foreignId('department_id')->constrained('departments');
            $table->timestamps();
        });
    }

The column names describe the information we will be gathering from the user.

Next, we will set up the eloquent relationships. In your app\Models\Staff.php add this method to define its relationship with the Department.php model

  public function department() {
        return $this->belongsTo(Department::class);
    }

We will also define the relationship in the Department model. Add this to the app\Models\Department.php

 public function staffs() {
        return $this->hasMany(Staff::class);
    }

Then we will set up our factory classes. The factory class for database/factories/StaffFactory.php will be

<?php

namespace Database\Factories;

use App\Models\Department;
use App\Models\Staff;
use Illuminate\Database\Eloquent\Factories\Factory;

class StaffFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Staff::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'staff_id' => rand(100, 1000),
            'date_of_birth' => $this->faker->date('Y-m-d', 'now'),
            'department_id'=> Department::all()->random()->id
        ];
    }
}

The factory class for database/factories/DepartmentFactory.php will be

<?php

namespace Database\Factories;

use App\Models\Department;
use Illuminate\Database\Eloquent\Factories\Factory;

class DepartmentFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Department::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'description' => $this->faker->sentence
        ];
    }
}

Now run

php artisan migrate

Then seed your from the Artisan tinker by running this command

Department::factory()->times(10)->create()

and

Staff::factory()->count(3)->forDepartment()->create()

Now that we have this basic setup, let's dive into the real deal. We'll then set up the routes To fetch all the departments we will set up a route as seen below

Route::get('departments', [DepartmentController::class, 'index']);

and the index method of the DepartmentController.php will be as seen below

 public function index()
    {
        $department = Department::all();
       return response()->json([
           'data' => $department
       ], 200);
    }

To fetch all the staff we will set up a route as seen below

Route::get('staff', [StaffController::class, 'index']);

and the index method of the StaffController.php will be as seen below

 public function index()
    {
        $staff = Staff::with('department')->get();
       return response()->json([
           'data' => $staff
       ], 200);
    }

And the response that we will get will be

{
    "data": [
        {
            "id": 1,
            "name": "Zelda Paucek",
            "email": "bednar.brandt@example.com",
            "staff_id": "122",
            "date_of_birth": "2008-05-01 00:00:00",
            "department_id": 4,
            "created_at": "2020-09-15T06:31:04.000000Z",
            "updated_at": "2020-09-15T06:31:04.000000Z",
            "department": {
                "id": 4,
                "name": "Luettgen LLC",
                "description": "Libero accusamus dicta quos.",
                "created_at": "2020-09-15T06:30:01.000000Z",
                "updated_at": "2020-09-15T06:30:01.000000Z"
            }
        },
        {
            "id": 2,
            "name": "Dr. Tommie Nikolaus",
            "email": "violet21@example.net",
            "staff_id": "161",
            "date_of_birth": "2011-03-30 00:00:00",
            "department_id": 3,
            "created_at": "2020-09-15T06:31:05.000000Z",
            "updated_at": "2020-09-15T06:31:05.000000Z",
            "department": {
                "id": 3,
                "name": "Gutkowski-Von",
                "description": "Cumque repudiandae velit voluptatum nulla magni.",
                "created_at": "2020-09-15T06:30:00.000000Z",
                "updated_at": "2020-09-15T06:30:00.000000Z"
            }
      ]
}

Returning this as a response is a bad practice because it exposes our database schema. To prevent this we will be creating an interface for the Department Controller and Staff Controller.

Creating the Interface

First, we will create an abstract class called Transformer.php, in a separate folder, but still under the app folder. In that folder, we will also be having DepartmentTransformer.php and StaffTransformer.php which are the transformer classes (API interfaces) for the DepartmentController.php and StaffTransformer.php classes. These classes will inherit the abstract Transformer.php class. Hence the folder will have this structure below. transformers.png

I placed the Transformers folder in a folder with my name Jed but feel free to name yours any name that you are comfortable with.

Place the following code in the Transformer.php

<?php

namespace App\Jed\Transformers;

use Illuminate\Database\Eloquent\Collection;

abstract class Transformer {

    public function transformCollection(Collection $items) {
        return $items->map([$this, 'transform']);
    }

    public abstract function transform($item);
}

Next, we will create an interface for the DepartmentController.php, this interface will mask the column names in our API response.

<?php

namespace App\Jed\Transformers;

class DepartmentTransformer extends Transformer
{
    public function transform($department)
    {
        return [
            'name' => $department['name'],
            'brief_description' => $department['description']
        ];
    }
}

Notice here that we masked the column name description with brief_description(though not the best of masks), this hides the column name and displays "brief_description" in the response instead as shown below. Notice also that any field that we don't want to include in the response will not be mapped to a key. We can do this for all responses.

Nested JSON Object

But what of nested JSON objects, say we want to get all staff with their department name. We will create a StaffTransformer.php in the transformer folder, then we will have the following lines of code in the StaffTransformer.php

<?php

namespace App\Jed\Transformers;

use App\Models\Department;

class StaffTransformer extends Transformer
{
    public function transform($staff)
    {
        return [
            'name' => $staff['name'],
            'email' => $staff['email'],
            'staff_id' => $staff['staff_id'],
            'department_name'=> $staff->department->name
        ];
    }
}

Notice here that we are only returning the department's name because that is what is necessary for this response.

Now let's use this in our Controllers.

<?php

namespace App\Http\Controllers;

use App\Models\Department;
use Illuminate\Http\Request;
use App\Jed\Transformers\DepartmentTransformer;

class DepartmentController extends Controller
{

     /**
     * @var App\Jed\Transformers\DepartmentTransformer
     */
    protected $departmentTransformer;

    function __construct(DepartmentTransformer $departmentTransformer)
    {
        $this->departmentTransformer = $departmentTransformer;
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
        $departments = Department::all();
        return response()->json([
            'data' => $this->departmentTransformer->transformCollection($departments)
        ], 200);
    }
// The remaining methods for this controller
}

The StaffController.php

<?php

namespace App\Http\Controllers;

use App\Models\Staff;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Jed\Transformers\StaffTransformer;

class StaffController extends Controller
{
        /**
     * @var App\Jed\Transformers\DepartmentTransformer
     */
    protected $staffTransformer;

    function __construct(StaffTransformer $staffTransformer)
    {
        $this->staffTransformer = $staffTransformer;
    }
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
        $staff = Staff::with('department')->get();
       return response()->json([
           'data' => $this->staffTransformer->transformCollection($staff)
       ], 200);
    }
// The remaining methods for this controller
}

Now let's see the interface at work,

{
"data": [
        {
            "name": "Zelda Paucek",
            "email": "bednar.brandt@example.com",
            "staff_id": "122",
            "department_name": "Luettgen LLC"
        },
        {
            "name": "Dr. Tommie Nikolaus",
            "email": "violet21@example.net",
            "staff_id": "161",
            "department_name": "Gutkowski-Von"
        },
        {
            "name": "Lexus Bogisich",
            "email": "kasey04@example.com",
            "staff_id": "476",
            "department_name": "Luettgen LLC"
        }
     ]
}

You can find the sample code for this article here

Conclusion

Creating an interface for your API responses decouples your API from the database schema. It allows you to make changes to your database without breaking the client (what consumes your API).

No Comments Yet