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
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,
- it exposes our database schema to anyone that has access to the API
- 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)
- 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
- A basic understanding of OOP (Object Oriented Programming) concepts like Abstract classes, Interfaces and Inheritance.
- A reasonable knowledge of PHP
- 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
- view all staff
- view all departments
- 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.
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"
}
]
}
Github Link
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).