DEV Community

Cover image for Laravel 8: REST Api with Resource Controllers
Sean Kerwin
Sean Kerwin

Posted on

Laravel 8: REST Api with Resource Controllers

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

/vendor/bin/phpunit
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
        });
    }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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])
        ];
    }
Enter fullscreen mode Exit fullscreen mode

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

\App\Models\Employee::factory()->make();
Enter fullscreen mode Exit fullscreen mode

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,
   }

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Add this line to the to TestCase

class EmployeeTest extends TestCase
{
    use RefreshDatabase, WithFaker;
}
Enter fullscreen mode Exit fullscreen mode

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
        ]);

    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
                ]
            ]);
    }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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",
            ]
        ]);
    }
Enter fullscreen mode Exit fullscreen mode

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",
            ]
        ]);
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
            ]
        ]);
    }
Enter fullscreen mode Exit fullscreen mode

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"
                ]
            ]);
    }
Enter fullscreen mode Exit fullscreen mode

Run the test for showing a single employee

/vendor/bin/phpunit --filter=test_can_get_a_single_employee
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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  
  ]);  
}
Enter fullscreen mode Exit fullscreen mode

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"  
  ]  
        ]);  
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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  
  ]);  
}
Enter fullscreen mode Exit fullscreen mode

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);  

}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

You can test the entire folder by running

/vendor/bin/phpunit --filter=EmployeeTest
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode

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 (1)

Collapse
mehrdadweb profile image
Mehrdad

tnx this article is really helpful