Designing Laravel REST API: Best practices

In modern application, API is one of the main feature in the application. It’s not only for creating mobile applications, desktop applications but also important for self-hosted web applications.

Now front-end and backend applications development has great changes using Vue or React front-end framework. All new applications want to single page application. That’s why implement API in the backend is the main feature.

Basically, an API is an interface that returns data in a special format that any kind of application, either it’s an Android app or a web app, can understand.

When developing an API, you need to take into considerations some best practices which follow different developer. I have research on web tutorials and sort out some best practices which I follow in my Laravel applications.

Best practices for developing Laravel rest API

Coding standards

It’s not only related to designing rest API, but it’s related to all kind of applications. Whenever I start building some application I start reading the coding standard of the application even I read it before. It’s forced me to follow the coding standard. Some of are related to designing rest API in Laravel.

Use routes/api.php file for API routes

Laravel be default has a separateroutes/api.php file that defers from the usual routes/web.php file. I think we must store our API routes in this file. It has an onboard applied middleware (which can be seen inapp/Http/Kernel.php the $middlewareGroups variable, under api) and a prefix of/api so all routes defined are already available to /api.

Route names with a api prefix

What I like to do, is to set a group route an as setting to the whole API, so I can access the routes by their name, with api. prefix.

Route::get('/users', "API\V1\[email protected]")->name("users");

This route’s URL can be get using route('users') but it might conflict with web.php.

Route::group(['as' => 'api.'], function () {
  Route::get('/users', 'API\V1\[email protected])->name('users');
});

This way, you will have it on route('api.users').

If you use the route namings, if you write tests, you won’t need to replace the URL everywhere if you plan to change the URL location and keep the route name.

Use plurals to describe resources

When you write resource route you should write plurals.

Route::resource('/users', 'API\V1\UserController’)->name('users');
// mysite.com/api/v1/customers

API Versioning

Remember the API will be used by other programs. We occasionally update the code on the server but it will break the client’s applications. This is when API versioning comes in handy. If your API is not publicly accessible you can keep as default. Our above URL when versioning is considered will look as follows.

mysite.com/api/v1/customers

Use Passport instead of JWT for API authentication

I personally like the Passport for authentication. It’s reliable and has support for Laravel developer.

Use a transformer

I always use a transformer to get data in one format. Although Laravel has own transformer class I personally like league/fractal

Response Codes and Error Handling

HTTP status codes will simplify giving feedback to your API users. Also you have to give a server message with a response. To handle errors, transformer and keep similar data set I have write a controller that is responsible to handle errors.

Limit the number of request in a given time period from the same IP Address

If it’s exposed to the public online then it is a target for spam, bots and abuse. You can limit to a request made per second to say maybe 5. Anything more than that may be an indicated of automated programs abusing your API.

<?php

namespace App\Http\Controllers\Api\V1;

use League\Fractal\Manager;
use League\Fractal\Resource\Item;
use App\Http\Controllers\Controller;
use League\Fractal\Resource\Collection;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;

class ApiController extends Controller
{
    /**
     * @var int $statusCode
     */
    protected $statusCode = 200;

    const CODE_WRONG_ARGS = 'GEN-FUBARGS';
    const CODE_NOT_FOUND = 'GEN-LIKETHEWIND';
    const CODE_INTERNAL_ERROR = 'GEN-AAAGGH';
    const CODE_UNAUTHORIZED = 'GEN-MAYBGTFO';
    const CODE_FORBIDDEN = 'GEN-GTFO';
    const CODE_INVALID_MIME_TYPE = 'GEN-UMWUT';

    /**
     * @var Manager $fractal
     */
    protected $fractal;

    public function __construct()
    {
        $this->fractal = new Manager;

        if (isset($_GET['include'])) {
            $this->fractal->parseIncludes($_GET['include']);
        }
    }

    /**
     * Get the status code.
     *
     * @return int $statusCode
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * Set the status code.
     *
     * @param $statusCode
     * @return $this
     */
    public function setStatusCode($statusCode)
    {
        $this->statusCode = $statusCode;

        return $this;
    }

    /**
     * Repond a no content response.
     * 
     * @return response
     */
    public function noContent()
    {
        return response()->json(null, 204);
    }

    /**
     * Respond the item data.
     *
     * @param $item
     * @param $callback
     * @return mixed
     */
    public function respondWithItem($item, $callback, $message = 'Successfully')
    {
        $resource = new Item($item, $callback);

        $data = $this->fractal->createData($resource)->toArray();

        $data['message'] = $message;

        return $this->respondWithArray($data);
    }

    /**
     * Respond the collection data.
     *
     * @param $collection
     * @param $callback
     * @return mixed
     */
    public function respondWithCollection($collection, $callback, $message = 'Successfully')
    {
        $resource = new Collection($collection, $callback);

        $data = $this->fractal->createData($resource)->toArray();
        $data['message'] = $message;

        return $this->respondWithArray($data);
    }

    /**
     *  Respond the collection data with pagination.
     *
     * @param $paginator
     * @param $callback
     * @return mixed
     */
    public function respondWithPaginator($paginator, $callback, $message = 'Successfully')
    {
        $resource = new Collection($paginator->getCollection(), $callback);

        $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));

        $data = $this->fractal->createData($resource)->toArray();
        $data['message'] = $message;

        return $this->respondWithArray($data);
    }

    /**
     * Respond the data.
     *
     * @param array $array
     * @param array $headers
     * @return mixed
     */
    public function respondWithArray(array $array, array $headers = [])
    {
        return response()->json($array, $this->statusCode, $headers);
    }

    /**
     * Respond the message.
     * 
     * @param  string $message
     * @return json
     */

    public function respondWithMessage ($message) {
        return $this->setStatusCode(200)
            ->respondWithArray([
                    'message' => $message,
                ]);
    }

    /**
     * Respond the error message.
     * 
     * @param  string $message
     * @param  string $errorCode
     * @return json
     */
    protected function respondWithError($message, $errorCode, $errors = [])
    {
        if ($this->statusCode === 200) {
            trigger_error(
                "You better have a really good reason for erroring on a 200...",
                E_USER_WARNING
            );
        }

        return $this->respondWithArray([
            'errors'  => $errors,
            'code'    => $errorCode,
            'message' => $message,
        ]);
    }

    /**
     * Respond the error of 'Forbidden'
     * 
     * @param  string $message
     * @return json
     */
    public function errorForbidden($message = 'Forbidden', $errors = [])
    {
        return $this->setStatusCode(500)
                    ->respondWithError($message, self::CODE_FORBIDDEN, $errors);
    }

    /**
     * Respond the error of 'Internal Error'.
     * 
     * @param  string $message
     * @return json
     */
    public function errorInternalError($message = 'Internal Error', $errors = [])
    {
        return $this->setStatusCode(500)
                    ->respondWithError($message, self::CODE_INTERNAL_ERROR, $errors);
    }

    /**
     * Respond the error of 'Resource Not Found'
     * 
     * @param  string $message
     * @return json
     */
    public function errorNotFound($message = 'Resource Not Found', $errors = [])
    {
        return $this->setStatusCode(404)
                    ->respondWithError($message, self::CODE_NOT_FOUND, $errors);
    }

    /**
     * Respond the error of 'Unauthorized'.
     * 
     * @param  string $message
     * @return json
     */
    public function errorUnauthorized($message = 'Unauthorized', $errors = [])
    {
        return $this->setStatusCode(401)
                    ->respondWithError($message, self::CODE_UNAUTHORIZED, $errors);
    }

    /**
     * Respond the error of 'Wrong Arguments'.
     * 
     * @param  string $message
     * @return json
     */
    public function errorWrongArgs($message = 'Wrong Arguments', $errors = [])
    {
        return $this->setStatusCode(400)
                    ->respondWithError($message, self::CODE_WRONG_ARGS, $errors);
    }
}

Security

The biggest problem you should be take care of is security. Laravel application is easy to secure it, but if not doing it properly, you might get hacked. In Laravel, you might want to use Laravel Passport — it belongs to the Laravel ecosystem, supports authentication through that App ID — App Secret thingy in order to get an access token, either to impersonate somebody or a server, either it’s backend or frontend.

Too hard to understand? Reach me!

If you have more questions about Laravel, if you need help with any information related to Laravel you can get in touch.

Say Thank you.

If any mistake please mention in the comment. If you like it you can encourage me saying Thank you.

Leave a comment

Your email address will not be published. Required fields are marked *