loading...
Cover image for Laravel 8: REST Api with Resource Controllers

Laravel 8: REST Api with Resource Controllers

seankerwin profile image Sean Kerwin ・12 min read

In this guide, I'm going to create a small Laravel 8 application to show how to use a Resource controller with a RESTful api and some basic CRUD functions.

This guide was written from my point of view and my development environment, I use a mac, and have Valet installed with MySQL/PHP installed via homebrew.

To create a project, you can run

composer create-project --prefer-dist laravel/laravel laravel-8-resource-controllers

Once that has all been created, there's a few things we need to setup, we need to link our new application to our database, so go ahead and create an SQL database and put the credentials in the .env file of your application.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=<your-database-name>
DB_USERNAME=<your-database-user>
DB_PASSWORD=<your-database-password>

I'll be using a few tests to make sure what we're creating works, I don't like using a real database to test my code, it's a lot easier, faster and generally a better process to use tests for our code.

To use an in-memory SQLite database for testing, open up phpunit.xml and uncomment the 2 lines in the XML file.

This is how my phpunit.xml file looks

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

To check everything is working, Laravel comes with a default test, you can simply run

/vendor/bin/phpunit

from within your applications directory and it will run the example test, if you see green, everything is setup!

PHPUnit 9.4.0 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:00.192, Memory: 22.00 MB
OK (2 tests, 2 assertions)

Now that we have everything setup and ready, for the sake of this guide, I'm going to be using an Employee model, this application will have CRUD functions, think of it as a database of employees.

Open up a terminal and run the following command

php artisan make:model Employee -a

The -a flag at the end is to create some extra files we'll need, it will create a migration and a resourceful controller, a factory and a seeder

Open up the newly created migration table for an Employee and add any fields you want against an employee.

Here's mine:

public function up()
    {
        Schema::create('employees', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->string('job_title')->nullable();
            $table->decimal('pay_rate', 11, 2)->default(0);
            $table->boolean('active')->default(true);
            $table->softDeletes();
            $table->timestamps();
        });
    }

You may have noticed that I have included a softDeletes column. This is purely for this demo of CRUD functions, but in reality, with any API that I build, I wouldn't actually delete the record, I'd always use SoftDeletes, to use SoftDeletes, add the trait to the Employee model.

class Employee extends Model
{
    use HasFactory, SoftDeletes;
}

Now we're going to setup our EmployeeFactory, If you don't know what a factory is, it's an easy way to create an instance of a model, which is especially handy for testing.

You can read more about factories on the official docs here.

Go ahead and open up the factory, we'll use faker to give us random information.

Inside the return array, I have added the following to mine:

    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'job_title' => $this->faker->jobTitle,
            'pay_rate' => $this->faker->randomFloat(2, 10, 20),
            'active' => $this->faker->randomElement([true, false])
        ];
    }

To see this in action, you can open up tinker and run the following:

\App\Models\Employee::factory()->make();

And you'll get something back along the lines of this:

=> App\Models\Employee {#3284
     name: "Reyes Donnelly PhD",
     job_title: "Health Specialties Teacher",
     pay_rate: 10.64,
     active: false,
   }

We're going to go ahead now and write our first test, yes, we're going to write a test before we write any actual logic, this is the core principle of test driven development.

Create our first test file:

php artisan make:test EmployeeTest

Open this file and remove the testExample() that is there.

Add this line to the to TestCase

class EmployeeTest extends TestCase
{
    use RefreshDatabase, WithFaker;
}

Create

Our first test is to make sure we can create an employee;

Here's the basics of our first test:

public function test_can_create_an_employee() {

        // make an instance of the Employee Factory
        $employee = Employee::factory()->make([
            'active' => true
        ]);

        // post the data to the employees store method
        $response = $this->post(route('employees.store'), [
            'name' => $employee->name,
            'pay_rate' => $employee->pay_rate,
            'job_title' => $employee->job_title,
            'active' => $employee->active
        ]);

        $response->assertSuccessful();

        $this->assertDatabaseHas('employees', [
            'name' => $employee->name,
            'pay_rate' => $employee->pay_rate,
            'job_title' => $employee->job_title,
            'active' => $employee->active
        ]);

    }

Let's break this test down, first we call an instance of the Employee factory, but we use the make() instead of the create(). Using create() will actually write this employee to the database, we don't want to do that, we just need the values from the EmployeeFactory, this is why we use the make().

I have added the active => true inside the make() method, as this passes overrides to the factory. If you go back and look at the factory, i have set it up to choose either true or false at random. Passing in true on the make() method will force it to always be true.

I then do a POST request to employee.store - This is a named route and we'll get more into this shortly. I have passed into the POST request the fields I need to actually create the Employee.

I then assert that my response is successful, and then assert that the database, employees table contains the data from the $employee factory we created.

If you run this test on its own:

/vendor/bin/phpunit --filter=test_can_create_an_employee

You should get the following error:


There was 1 error:

1) Tests\Feature\EmployeeTest::test_can_create_an_employee
Symfony\Component\Routing\Exception\RouteNotFoundException: Route [employees.store] not defined.

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

Ok, so we don't have a route called employees.store, open up our api.php file:

All we need is this:

// Employee
Route::resource('employees', EmployeeController::class);

Laravel will handle all of the named routes for standard CRUD functions itself.

The http methods for CRUD functions are as follows:

POST = create
GET = read
PATCH = update
DELETE = delete

Laravel's resource controller comes with some methods that we will use, index, show, store, update, destroy.

Using laravel's named routes, for testing we can use the following:

POST to employees.store = create()
GET to employees.show = index() or show()
PATCH to employees.update = update(),
DELETE to employees.destroy = destroy()

Before we get into the EmployeeController, here's a good time to create a Transformer, I've only just started using Transformer and they're amazing, trust me!

Create a new directory manually inside the Http folder called Transformers, inside this folder, create a new file called EmployeeTransformer.php

<?php

namespace App\Http\Transformers;

use App\Models\Employee;

class EmployeeTransformer
{

    public static function toInstance(array $input, $employee = null)
    {
        if (empty($employee)) {
            $employee = new Employee();
        }

        foreach ($input as $key => $value) {
            switch ($key) {
                case 'name':
                    $employee->name = $value;
                    break;
                case 'pay_rate':
                    $employee->pay_rate = $value;
                    break;
                case 'job_title':
                    $employee->job_title = $value;
                    break;
                case 'active':
                    $employee->active = $value;
                    break;
            }
        }

        return $employee;
    }
}

Basically this transformer will either create a new Employee and assign the values it's given, or if an Employee is passed into the transformer, it will update the values it's given.

We can optionally create an EmployeeResource with the following:

php artisan make:resource EmployeeResource

You don't have to create resources for your models, I like to do it for sake of convenience.

Now head back to our EmployeeController and look at the store method.

    /**
     * Store a newly created resource in storage.
     *
     * @param  Request  $request
     * @return EmployeeResource|JsonResponse
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string',
            'pay_rate' => 'required|numeric',
            'job_title' => 'required|string',
            'active' => 'required|boolean',
        ]);

        if ($validator->fails()) {
            return response()->json($validator->errors()->toArray(), 422);
        }

        DB::beginTransaction();
        try {
            $employee = EmployeeTransformer::toInstance($validator->validate());
            $employee->save();
            DB::commit();
        } catch (Exception $ex) {
            Log::info($ex->getMessage());
            DB::rollBack();
            return response()->json($ex->getMessage(), 409);
        }

        return (new EmployeeResource($employee))
            ->additional([
                'meta' => [
                    'success' => true,
                    'message' => "employee created"
                ]
            ]);
    }

To break this down quickly, we have a validator at the top that checks the $request->input()

We then use a Database Transaction, and a try/catch.
Inside the try, we assign $employee to a new EmployeeTransformer passing in the $validator->validate() fields.

And then finally return the EmployeeResource passing in the newly created $employee.

If you have all of this, run the test again and you should see green.

PHPUnit 9.4.0 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)

Time: 00:00.343, Memory: 30.00 MB
OK (1 test, 2 assertions)

This sums up the Create part of CRUD.

Read

Reading is really easy, first, lets create the test in our EmployeeTest.php

public function test_can_get_paginated_list_of_all_employees()
    {
        // Create 25 Employees in the database
        Employee::factory()->count(25)->create();

        // Get all Employees (Paginated)
        $response = $this->get(route('employees.index'));
        $response->assertSuccessful();

        $response->assertJsonStructure([
            'data' => [
                '*' => [
                    'id',
                    'name',
                    'job_title',
                    'pay_rate',
                    'active',
                    'deleted_at',
                    'created_at',
                    'updated_at',
                ]
            ],
            'links' => [
                'first',
                'last',
                'prev',
                'next',
            ],
            'meta' => [
                "current_page",
                "from",
                "path",
                "per_page",
                "to",
                "success",
                "message",
            ]
        ]);
    }

Your index() method of the EmployeeController should look something like this

public function index(Request $request)
    {
        return EmployeeResource::collection(
            Employee::simplePaginate($request->input('paginate') ?? 15)
        )->additional([
            'meta' => [
                'success' => true,
                'message' => "employees loaded",
            ]
        ]);
    }

Basically, we're returning a collection of the EmployeeResource, passing in the Employee model, and using the simplePaginate trait, we're also assigning a default limit of 15 unless one is specified in a URL Param.

The URL of this request would be

GET: api/employees?paginate=15

Run the test for this:

/vendor/bin/phpunit --filter=test_can_get_paginated_list_of_all_employees

And we see green!

PHPUnit 9.4.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)
Time: 00:00.478, Memory: 30.00 MB
OK (1 test, 136 assertions)

To read a specific Employee lets first create another test.

public function test_can_get_a_single_employee()
    {
        $employee = Employee::factory()->create();
        $response = $this->get(route('employees.show', $employee->id));
        $response->assertSuccessful();
        $response->assertJson([
            'data' => [
                'id' => $employee->id,
                'name' => $employee->name,
                'job_title' => $employee->job_title,
                'pay_rate' => $employee->pay_rate,
                'active' => $employee->active
            ],
            'meta' => [
                'success' => true
            ]
        ]);
    }

Almost the same as the one for the paginated list, except here we assign $employee to the factory that creates an employee in the database, we're calling the URI for employees.show and passing in the $employee->id.

This translates to the following URI

Route: employees.show
URI: api/employees/{employee}
Method: GET

The show() method on the EmployeeController is probably one of the smallest:

    /**
     * Display the specified resource.
     *
     * @param  Employee  $employee
     * @return EmployeeResource
     */
    public function show(Employee $employee)
    {
        return (new EmployeeResource($employee))
            ->additional([
                'meta' => [
                    'success' => true,
                    'message' => "employee found"
                ]
            ]);
    }

Run the test for showing a single employee

/vendor/bin/phpunit --filter=test_can_get_a_single_employee

and the result

PHPUnit 9.4.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.333, Memory: 30.00 MB
OK (1 test, 2 assertions)

This sums up the read part of this guide.

Update

Update is a good one! It's almost the same as the store/create part aside from some minor changes.

Let's start by creating our test.

public function test_can_update_an_employee()  
{  
    $employee = Employee::factory()->create([  
        'active' => true  
  ]);  

    $response = $this->patch(route('employees.update', $employee->id), [  
        'name' => $name = $this->faker->name,  
        'job_title' => $job_title = $this->faker->jobTitle,  
        'pay_rate' => $pay_rate = $this->faker->randomFloat(2, 10, 20),  
        'active' => false  
  ]);  

    $response->assertSuccessful();  
    $this->assertDatabaseHas('employees', [  
        'id' => $employee->id,  
        'name' => $name,  
        'job_title' => $job_title,  
        'pay_rate' => $pay_rate,  
        'active' => false  
  ]);  
}

This test is slightly different, we create an employee using the Employee::factory(), then we do a Patch request to what the URI is essentially employees/{employee}.

We're passing in new data, but you can see here I've assigned the new data to variables, this is so later in my test, I assert that the database has these new values.

Basically, $name gets assigned to $this->faker->name and so on...

Back over to our EmployeeController and we're going to work on the update() method.

/**  
 * Update the specified resource in storage. * * @param Request $request  
  * @param Employee $employee  
  * @return JsonResponse|EmployeeResource  
 */public function update(Request $request, Employee $employee)  
{  
    $validator = Validator::make($request->all(), [  
        'name' => 'sometimes|required|string',  
        'pay_rate' => 'sometimes|required|numeric',  
        'job_title' => 'sometimes|required|string',  
        'active' => 'sometimes|required|boolean',  
    ]);  

    if ($validator->fails()) {  
        return response()->json($validator->errors()->toArray(), 422);  
    }  

    DB::beginTransaction();  
    try {  
        $updated_employee = EmployeeTransformer::toInstance($validator->validate(), $employee);  
        $updated_employee->save();  
        DB::commit();  
    } catch (Exception $ex) {  
        Log::info($ex->getMessage());  
        DB::rollBack();  
        return response()->json($ex->getMessage(), 409);  
    }  

    return (new EmployeeResource($updated_employee))  
        ->additional([  
            'meta' => [  
                'success' => true,  
                'message' => "employee updated"  
  ]  
        ]);  
}

This is almost the same as the store() method on the controller, we have some changes. The Validator rules have the extra sometimes bits, with a PATCH request, you're only supposed to pass the bits of data you want to update, if you want to update the entire thing and will be providing all of the keys, you would use a PUT request instead.

The sometimes bit in the validator basically means, if the key is present in the request, continue with the validation, if not, don't worry.

We do a transaction, like we did in the store() method, here we've assigned it to the variable $updated_employee, this is because $employee is being eager loaded in the method.

We then pass the validated data, and the $employee into the Employee transformer, which will update all the values that it has been given.

$updated_employee is now the $employee with updated data, we then save it, and return the EmployeeResource again, passing in the $updated_employee.

Back to our test, run the test for updating an employee

/vendor/bin/phpunit --filter=test_can_update_an_employee

and we should see green!

PHPUnit 9.4.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.383, Memory: 30.00 MB
OK (1 test, 2 assertions)

This concludes the update part of the CRUD functions.

Delete

I said at the beginning that I don't like completely destroying data in a database, I try to use softDeletes where possible. If you've never used soft deletes, think of these like the trash bin on your computer, you can delete a file, but you can also recover it if needed.

You can read more about softDeletes over at the Laravel docs here.

If you don't choose to use softDeletes, the rest of the controller method is the same, even if you are hard deleting data.

Let's start by creating the test!

public function test_can_delete_an_employee()  
{  
    $employee = Employee::factory()->create([  
        'active' => true  
  ]);  
    $response = $this->delete(route('employees.destroy', $employee->id));  
    $response->assertSuccessful();  
    $this->assertSoftDeleted('employees', [  
        'id' => $employee->id,  
        'active' => false  
  ]);  
}

Simple test, like the others, we create an employee in the database, with the active flag set to true, we then send a delete request to the named route employees.delete passing in the employee->id,

We then do a database assertion that it has been softDeleted, i have also asserted that the active flag should be false. This is down to business/company logic, but in this example, if an employee has been deleted then they're essentially inactive.

Back over to the EmployeeController and the destroy() method:

/**  
 * Remove the specified resource from storage. 
 * @param Employee $employee  
 * @return JsonResponse  
 */
public function destroy(Employee $employee)  
{  
    DB::beginTransaction();  
    try {  
        $employee->delete();  
        $employee->active = false;  
        $employee->save();  
        DB::commit();  
    } catch (Exception $ex) {  
        Log::info($ex->getMessage());  
        DB::rollBack();  
        return response()->json($ex->getMessage(), 409);  
    }  

    return response()->json('employee has been deleted', 204);  

}

Simple database transaction where we're calling the delete() method on the $employee, again, using soft-deletes, this doesn't actually delete the model, but sets a deleted_at timestamp on the table in the database, Laravel will take that if there is a timestamp set in that field, then that record has been deleted.

You can still restore this record, but that's out of the scope of this tutorial, however, you can read about record restoring here.

Run the tests for this delete method and we should see green!

PHPUnit 9.4.0 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 00:00.653, Memory: 30.00 MB
OK (1 test, 2 assertions)

You can test the entire folder by running

/vendor/bin/phpunit --filter=EmployeeTest
PHPUnit 9.4.0 by Sebastian Bergmann and contributors.
.....                                                               5 / 5 (100%)
Time: 00:01.282, Memory: 32.00 MB
OK (5 tests, 144 assertions)

Conclusion

There you have it, simple test-driven development of a basic CRUD function.

Whilst this might seem daunting and a lot of work to do, in reality, it becomes second nature the more you do it. I am by no means a PHP/Laravel expert, but If i wasn't writing this guide alongside actually writing the code, I can do this in under 10 minutes.

There is still plenty of room for improvement, we're missing some key things such as Authentication and better error handling, but this should provide the basis of what I consider a simple, concise and clear TDD RESTful API.

You can check the code for this out over at my repo:
github.com/lordkerwin/laravel-8-resource-controllers

Thanks for reading!

Discussion

pic
Editor guide