laravel api

Our goal is to extend the framework in order to create a Laravel API with features that are useful for all types of applications. It could be considered an RESTful API Starter Pack for which we need to cover some basic requirements on how to create one.

 

1. Standard Built-in Response Structure for Laravel API

Our response has to always follow the same structure. For example when we are returning a success response it might be like:

{
  status: success,
  data: {your-data}
}

 

For an error response it might be useful to send a status error alongside some details about errors:

{
    status: error,
    errors: [errors]
}

 

In order to achieve this we are creating an APIController containing some basic methods for handling responses, and all the other controllers will extend this one.

class APIController extends Controller
{	
	protected $statusCode = HttpResponse::HTTP_OK;

	public function getStatusCode()
	{
		return $this->statusCode;
	}

	public function setStatusCode($statusCode) 
	{
		$this->statusCode = $statusCode;
	}

	public function respond($data = [], $headers = [])
	{
		return Response::json([
			'status' => 'success',
			'data'   => $data
		], $this->getStatusCode(), $headers);
	}
}

 

Each time we need to send a response to the client we’ll simply call the respond method like this:

$this->respond([
    'hello' => 'world'
]);

 

We can of course extend our respond method to add some specific actions. For example when an entity is created on the server after a client’s request we have to send back a status code 201, so we could implement the respondCreated method:

public function respondCreated()
{
	$this->setStatusCode(HttpResponse::HTTP_CREATED); // set status code to 201
	return $this->respond();
}

 

2. Engine for exception handling

We already established how the error response should look like but what we would like to build is an automatic way to catch Exceptions and send the response to the client. In order to achieve this we will extend the basic Exception with a new one called APIException. We also add a new functionality by giving the possibility to set multiple error messages rather than a single one:

class APIException extends Exception
{   
    protected $errors;
    protected $statusCode;

    public function __construct($errors, $statusCode) 
    {
        if (is_string($errors)) {
            $this->errors = [$errors];
        } else {
            $this->errors = $errors;
        }

        $this->statusCode = $statusCode;
    }

    public function getErrors()
    {
        return $this->errors;
    }

    public function getStatusCode()
    {
        return $this->statusCode;
    }
}

 

Now we need a way to listen for APIException and then build the proper response for the client. Laravel 5 provides an Exception Handler that is automatically called when an Exception is thrown. We have to check in the “render” method the type of Exception and send the response to the client:

public function render($request, Exception $e)
{   
    # if Exception is thrown in API return error Response
    if ($e instanceof APIException) {
            
        return Response::json([
             'status' => 'error',
             'errors' => $e->getErrors()
        ], $e->getStatusCode());

    } else {
        
        return parent::render($request, $e);
    }
}

 

When an APIException will be thrown the following response will be sent to the client:

#throwing this exception
throw new APIException("invalidCredentials", HttpResponse::HTTP_UNAUTHORIZED);

#will send to the client the following response with status code 401
{
  status: error
  errors: ["invalidCredentials"]
}

 

3. Automatic Validation

Our next goal is to create an automatic validation system for Models, so we could just register some rules and if the received input fails validation the response will automatically built and sent to the client.

Register validation rules:

public function login($request)
{
    $this->validate($request, [
        'email'    => 'required|email',
        'password' => 'required'
    ]);

    // the rest of the logic
}

 

If input is not valid the following response will be sent to the client:

{
  status: error,
  errors: ["requiredPassword", "invalidEmail"]
}

 

To achieve this we’ll build a Laravel API base model called Model and every others models will extend this one to have access to some specific methods like validate.

class Model extends Eloquent
{   
    # Custom validation messages for API errors status.
    private $messages = array(
        'required' => 'required :attribute',
        'email'    => 'invalid :attribute',
        'min'      => 'invalid :attribute',
        'max'      => 'invalid :attribute',
        'numeric'  => 'invalid :attribute',
        'unique'   => 'duplicated :attribute',
    );

    # Validate the given request with the given rules.
    public function validate(Request $request, array $rules, array $messages = [], array $customAttributes = [])
    {   
        $validator = $this->getValidationFactory()->make($request->all(), $rules, $this->messages, $customAttributes);

        if ($validator->fails()) {
            $errors = $this->formatValidationErrors($validator);
    
            // send errors to client by throwing an APIException
            throw new APIException($errors, HttpResponse::HTTP_BAD_REQUEST);
        }
    }

    # Format the validation errors to be returned in camelcase
    protected function formatValidationErrors(Validator $validator)
    {   
        $errors = $validator->errors()->all();
        $result = array();

        foreach ($errors as $error) {
            $result[] = Str::camel($error);
        }

        return $result;
    }
}

 

4. User Registration

We will configure the router to accept a POST Request to /api/auth/register:

Route::group(['prefix' => 'api'], function () {

    /**
     * Authentication routes
     */
    Route::group(['prefix' => 'auth'], function() {
    	Route::post('register',      'API\UsersController@register');
    });
});

 

In our UsersController we’ll add the register method as follows:

public function register(Request $request)
{	
	$user = new User;
	$user->register($request);

	return $this->respondCreated();
}

 

We structured user’s information into first name, last name, email and password, but they could be extended as needed. In the User model register method looks like:

public function register($request)
{   
    $this->validate($request, [
        'firstName' => 'required|max:255',
        'lastName'  => 'required|max:255',
        'email'     => 'required|email|unique:users,email|max:255',
        'password'  => 'required|max:255'
    ]);

    $this->first_name = $request->input("firstName");
    $this->last_name  = $request->input("lastName");
    $this->email      = $request->input("email");
    $this->password   = bcrypt($request->input("password"));

    $this->save();
}

 

5. User Authentication Based on JWT Standard

For JWT Authentication we’ll use an library created for Laravel API called JWT Auth.  Next we will configure the router to accept a POST Request to /api/auth/login:

Route::group(['prefix' => 'api'], function () {
    /**
     * Authentication routes
     */
    Route::group(['prefix' => 'auth'], function() {
    	Route::post('login',         'API\UsersController@login');
    	Route::post('register',      'API\UsersController@register');
    });
});

In our UsersController we’ll add the login method as follows:

public function login(Request $request)
{
	$user = new User;

	return $this->respond([
		'token' => $user->login($request)
	]);
}

In the User model login method looks like:

public function login($request)
{
    $this->validate($request, [
        'email'    => 'required|email',
        'password' => 'required'
    ]);

    if ($token = JWTAuth::attempt(['email' => $request->input("email"), 'password' => $request->input("password")])) {
        return $token;
    }
        
    throw new APIException("invalidCredentials", HttpResponse::HTTP_UNAUTHORIZED);
}

 

6. Token Refresh

The JWT Token has a limited lifetime so we have to provide a method to get a new token based on the old one, before its expiration. In order to do this in Laravel 5 we will configure the route /api/auth/refresh-token:

Route::group(['prefix' => 'api'], function () {
    /**
     * Authentication routes
     */
    Route::group(['prefix' => 'auth'], function() {
    	Route::post('login',         'API\UsersController@login');
    	Route::post('register',      'API\UsersController@register');
        Route::get('refresh-token',  'API\UsersController@refreshToken');
    });
});

In our UsersController we’ll add the refreshToken method as follows:

public function refreshToken(Request $request)
{
	$user  = new User;
		
	return $this->respond([
		'token' => $user->refreshToken($request)
	]);
}

In the User model register method looks like:

public function refreshToken($request)
{
    try {
        return JWTAuth::refresh(JWTAuth::getToken());
    } catch (Exception $e) {
        throw new APIException("invalidToken", HttpResponse::HTTP_UNAUTHORIZED);
    }
}

 

7. Token Invalidation

Our Laravel API needs a method to invalidate tokens in order to better control access to some resources. To achieve this we will configure the route /api/auth/logout:

Route::group(['prefix' => 'api'], function () {
    /**
     * Authentication routes
     */
    Route::group(['prefix' => 'auth'], function() {
    	Route::post('login',         'API\UsersController@login');
        Route::post('logout',        'API\UsersController@logout');
    	Route::post('register',      'API\UsersController@register');
        Route::get('refresh-token',  'API\UsersController@refreshToken');
    });
});

In our UsersController we’ll add the refreshToken method as follows:

public function refreshToken(Request $request)
{
	$user  = new User;
		
	return $this->respond([
		'token' => $user->refreshToken($request)
	]);
}

In the User model register method looks like:

public function refreshToken($request)
{
    try {
        return JWTAuth::refresh(JWTAuth::getToken());
    } catch (Exception $e) {
        throw new APIException("invalidToken", HttpResponse::HTTP_UNAUTHORIZED);
    }
}

 

You can see more information about how we designed this Laravel API Starter Pack by checking out our repository at https://github.com/agvision/Laravel_API.

2 Comments

  1. olivedev

    Rest API with Laravel is not really that difficult to start with, even for a new developer. However, the process is long. But with new packages, like Laravel Passport, you can do it much quicker than before. You can easily create REST API (example: https://www.cloudways.com/blog/rest-api-laravel-passport-authentication/) and do authentication using this package.

    • Alin Ghinoiu

      That’s correct, Oliver! That’s what we recommend nowadays.

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>