Cover
PHP

How to build and structure API's in Laravel (including authorization)

Introduction
How to build and structure API's in Laravel (including authorization)

In this blog post, I'd like to deviate a bit from the usual topics I blog about and instead focus on some lessons I've learned whilst setting up several API's in Laravel.  To do this I will first describe how I personally like to set up my controllers, after which I will go a bit into detail about authorization and lastly I will describe how I'd like to write my unit tests.

Choosing the type of controller

In Laravel, there are several types of controllers you can create:

  • Single action controller
    This is a controller that handles a single type of requests (e.g. GET /users). It can be identified by the presence of the __invoke() method.
  • Resource controller
    These controllers tend to have all the available CRUD operations for 1 specific Eloquent model in 1 file, where the methods adhere to a standardized naming scheme. This allows the developer to handle all the CRUD-related routes with a single entry in the routing (using Route::resource(/* */)).
  • Custom controller (e.g. defining your own functions)
    These are the controllers which don't adhere to the beforementioned categories, and which are custom made. It can be 'anything' that is wired up in the routing with a method that accepts a Request.

Personally I'm a big fan of the single action controller, this because it helps me to adhere to the Single Responsibility principle, however you are free to pick whatever suits your needs.

Now that we have selected the type of controller that we're going to use, it is time to look at how we're gonna obtain the input from the user.

Obtaining the input from the user

Laravel has made it very simple to obtain information about the request (e.g. there is no need to worry about differences between GET / POST requests). By default Laravel does this by passing a request to the controller (source). Whilst you can relatively easily validate these requests, it does start to blend responsibilities.

To tackle the blending of responsibilities, Laravel has introduced FormRequests. These FormRequests will handle the validation logic before the controller logic has been reached (thus the moment you reach the controller, there is no need to worry about validation).

We will first quickly dive into the response object, before further diving into and showing these FormRequests.

Returning a response to the user

Now that we know how to build a controller and how to obtain the input from the user, it is time to learn how to return a response to the user. As you might have figured, Laravel has made this very easy to do as well.

Returning a response can be as simple as:

<?php

return response()->json(['status' => __('httpstatus.success')], JsonResponse::HTTP_OK);
Returning a response in Laravel

However sometimes you have to return a resource, for this Laravel has invented Resource Responses. Personally I'm a big fan of these Resource Responses when building my API's because it allows me to further simplify the controller and further split the responsibilities.

Creating the controller

Now that we've seen several concepts, it is time to tidy things together. Let's take a look at an example where we're creating an API that allows users to create blog posts (for simplicity we'll only handle the creation controllers, however the general idea is applicable for the other type of controllers (read, update and deletion) as well).

<?php

// The FormRequest

namespace App\Blog\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateBlogRequest extends FormRequest
{
    // Leaving out the authorize method since we will handle this in the routing later on.

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
    	// See: https://laravel.com/docs/8.x/validation#available-validation-rules for a list of available validation rules.
        return [
            'title' => 'required|string|max:1024',
            'body' => 'required|string',
        ];
    }
}

?>

<?php

// The Resource Response object

namespace App\Blog\Resources;

use App\Blog\Model\Blog;
use App\Models\User;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Request;

class BlogResource extends JsonResource
{
    /** @var string */
    public $resource = Blog::class;

    /**
     * Transform the resource into an array.
     *
     * @param Request $request
     *
     * @return array
     */
    public function toArray($request): array
    {
        /** @var Blog $blog */
        $blog = $this->resource;

        return [
            'id' => $blog->id,
            'title' => $blog->title,
            'description' => $blog->description,
        ];
    }
}

?> 

<?php

// The controller

namespace App\Blog\Controllers;

use App\Blog\Model\Blog;
use App\Blog\Request\CreateBlogRequest;
use App\Blog\Resources\BlogResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Routing\Controller;

class CreateBlog extends Controller
{
    public function __invoke(CreateBlogRequest $request): JsonResource
    {
        /** @var User $user */
        $user = $request->user();
        
        $blog = Blog::create([
            // No need to do any validation since that has been handled by our request.
            'title' => $request->title,
            'description' => $request->description,
            'author_id' => $user->id,
        ]);

        return new BlogResource($blog);
    }
}

?>
Creation of the controller including the request and response

Whilst this demonstrates the most basic case, it still holds up for more complex cases as well. If you're using this for a more complicated flow it is good to know that Laravel tends to prefer to handle errors through exceptions. This does not mean that you can't return your own custom response()->json() responses, however it is good to keep this preference for exceptions in mind. In a later blog psot we will go more in depth about the workings of this (and what my personal stance on it is).

Now that we've created a controller, it is time to wire up this controller to be able to expose it to the world. Whilst doing this, we will also take a look at how to include the authorization layer for our controller; Let's keep going.

Exposing the controller to the clients

To expose the controller to possible clients, we have to add a new 'entry' in the app/routes/api.php file (which is the default location, however it is possible to split this up in sub files, but that is for a different blog). Laravel provides a rich set of features that you can add to the routing (for the full list, I'll refer you to the official documentation), for the sake of simplicity we will focus on the very basics for our use case.

For our CreateBlog controller, we will add the following entry in the api.php file:

<?php

// app/routes/api.php

use App\Blog\Controllers;

Route::post('blog', CreateBlog::class)
    // Example middleware that enforces:
    //     - The user to be authenticated with an API token (https://laravel.com/docs/8.x/sanctum)
    //     - The user has verified his/her email address
    ->middleware(['auth:sanctum', 'verified'])
    // Used as name when creating urls (e.g. handy in unit tests). 
    ->name('blog.create');
Registering the controller in the routing

Having added the above entry, it will allow us to send POST requests to localhost/api/blog which will then be resolved by the CreateBlog::__invoke() method.

Preventing unauthorized actions

Now lets imagine that we only want users with the name 'Bob' to be able to create new blogs. To do this, there are (again!) several ways to handle authorization in Laravel, however Policies tend to be my personal favourite due to the clear seperation of concerns and ease of integration in the application.

To get started with a Policy, it's quite straight forward. We have to create a BlogPolicy class (see below for the example), which we then have to 'link' to the Blog model. This can be done through the following methods:

  • By registering the Policy in the AuthServiceProvider.
  • By putting the Policy in the App/Policies directory
  • By putting the Policy in a Policies folder on the same level as the Model folder of the object for which we're creating a Policy.

    E.g. if we have the Blog model in App/Blog/Model/Blog.php then we can store the Policy in App/Blog/Policies/BlogPolicy.php.

Personally I prefer the last option, due to my preference to organize the project on a feature basis (which is a topic for another blog post), although you are free to pick whichever option suits you the best.

Lets take a look at an example of a BlogPolicy:

<?php

namespace App\Blog\Policies;

use App\Blog\Model\Blog;
use App\Models\User;

class BlogPolicy
{
    public function create(User $user, Blog $blog): bool
    {
        return $user->name === 'Bob';
    }
}
An example of the BlogPolicy

To 'weave' this Policy into the rest of the application (and really start using it), we have to go back to the api.php class which we just modified. In this class we have to modify the middleware([/* */]) function call. To integrate our just created Policy here, we have to update this middleware to the following (see the last entry in the array):

<?php

// app/routes/api.php

/* */
    // In cases where you have a specific model (e.g. read, update or delete);
    // You can replace the 'Blog::class' with the named parameter in the url, which would be 'blog' in our example.
    // See: https://laravel.com/docs/8.x/authorization#via-middleware
    ->middleware(['auth:sanctum', 'verified', 'can:create,' . Blog::class])
/* */
Adding the Policy to the middleware

Testing it all

Last but not least comes the creation of unit tests for the controller. Whilst I normally tend to prefer to create these tests before starting on the 'feature' (and thus follow TDD), I've decided to leave it as the last point for this article to improve the readability.

Personally I'm a big fan of structuring my tests as a set of business requirements, for example: 'Only users with the name Bob are allowed to create orders'. This helps me to keep a clear overview of what the class-under-test is supposed to be doing and it also helps me easily see what needs to be changed if certain new features are added.

Apart from structuring my tests by business requirements, I also tend to 'only' focus my test on the controller and have the created FormRequest, JsonResponse and Policy be tested as a result of the business requirements which are being tested in the controller (thus having an integration test). Ofcourse you are still free to test the individual components as well, but the reason that I tend to prefer this is that it gives me a better picture of the overal system and if I mis-wired something, it tends to show up sooner compared to mocking / testing everything individually (I will go into more detail about this in a separate blog post!).

Now that we've discussed my preferences for setting up these unit tests, it's time to look at the last concrete example for our beforementioned scenario:

<?php

namespace Tests\Feature\Blog\Controller;

use App\Blog\Model\Blog;
use App\Blog\Resources\BlogResource;
use App\Models\User;
use Faker\Factory;
use Illuminate\Auth\Authenticatable;
use Illuminate\Http\JsonResponse;
use Tests\TestCase;

class CreateBlogTest extends TestCase
{
    public function test_it_blocks_unauthenticated_users(): void
    {
        // Left out for the sake of keeping it readable.
    }

    public function test_it_allows_creation_of_blogs(): void
    {
        $faker = Factory::create();

        /** @var User|Authenticatable $user */
        $user = User::factory()->create(['name' => 'Bob']);
        $token = $user->createToken('unit-test', ['*']);

        // Using Faker to generate random test data for us.
        $postData = [
            'title' => $faker->text,
            'description' => $faker->text,
        ];

        $response = $this
            ->withHeader('Authorization', 'Bearer ' . $token->plainTextToken)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('Accept', 'application/json')
            ->postJson(route('blog.create'), $postData);

        $response->assertStatus(JsonResponse::HTTP_CREATED);
        $response->assertJson([
            'data' => (new BlogResource(Blog::first()))->jsonSerialize(),
        ]);
    }

    public function test_it_only_allows_creation_of_blogs_for_users_with_the_name_bob(): void
    {
        // Left out for the sake of keeping it readable.
    }

    public function test_it_prevents_faulty_input(): void
    {
        // Left out for the sake of keeping it readable.
    }

    // More tests left out for the sake of keeping it readable
}

Conclusion

That was it, now you know how I tend to structure my API code in a Laravel application.  I'm curious to hear if this post helped you gain insight for creating your next API controller in Laravel, or whether you have an entirely different viewpoint on this topic? Please let me know in the comments down below!

Until next time!

Vasco de Krijger
View Comments
Next Post

Dealing with datetimes and timezones in your application.

Previous Post

Software Philosophy: Data at the outer layer

Success! Your membership now is active.