PHP
Article

How to Build an API-Only JWT-Powered Laravel App

By Francesco Malatesta

JSON Web Tokens and Laravel API apps

The Laravel API Boilerplate (JWT Edition) was made on the shoulders of giants like:

In this article, we will learn how to use it to quickly create a fully functional API for an imaginary book wishlist application. As an aside, we will also see how to build a client application with AngularJS that will use our APIs.

Laravel Logo

Creating the Project

This tutorial assumes a working PHP environment has been set up, something like Homestead Improved. Let’s install our API Boilerplate. All we have to do is to clone the Github repository, with

git clone https://github.com/francescomalatesta/laravel-api-boilerplate-jwt Laravel

then run

composer install

to install dependencies and make all the basic configuration operations. Both Laravel and JWT keys will be automatically generated.

Everything went well

Nothing more to do! Let’s build the server part of our app!

Creating the API

Before doing anything, we have to plan our application’s details and features. We are going to build a basic book wishlist, so we will be using two main entities: User and Book.

A User will be able to:

  • sign up, in order to create a new account;
  • log in, if they already have an account;
  • log out, to leave the application;
  • get a list of their books;
  • add a new book to the wishlist;
  • edit an existing book;
  • delete an existing book from the list;

Note that for every User we will ask for:

  • name;
  • email address;
  • password;

A Book will have a:

  • title;
  • author name;
  • pages count;

Finally, every list will show books starting from the most recent one.

First step: the User part.

Building the User Part

The good news is that the User part is already completed.

If we open the app/Api/V1/Controllers/AuthController.php file, we can see that it already has a signup and a login method! They are not the the ones we can see in the standard AuthController in a standard new Laravel project: here they were re-implemented to have a token-based sign up and log in procedure.

Now, let’s take a look at the signup method, in particular.

public function signup(Request $request)
{
    $signupFields = Config::get('boilerplate.signup_fields');
    $hasToReleaseToken = Config::get('boilerplate.signup_token_release');

    $userData = $request->only($signupFields);

    $validator = Validator::make($userData, Config::get('boilerplate.signup_fields_rules'));

    if($validator->fails()) {
        throw new ValidationHttpException($validator->errors()->all());
    }

    User::unguard();
    $user = User::create($userData);
    User::reguard();

    if(!$user->id) {
        return $this->response->error('could_not_create_user', 500);
    }

    if($hasToReleaseToken) {
        return $this->login($request);
    }
    
    return $this->response->created();
}

As we can see from the code, we are using some options in a config file. Specifically, we are getting two values from this file:

  • boilerplate.signup_fields: the fields list we want to use for the signup operation;
  • boilerplate.signup_fields_rules: the validation rules we want our data checked against during the signup operation;

We can find all the boilerplate options in config/boilerplate.php.

There’s also another option used here: boilerplate.signup_token_release. When set to true, a token is automatically released after signup. This means that the user can start using your app immediately.

Note: for this project we are going to use 24-hour tokens. The default value for the token’s life-span is 60 minutes. This can be changed in config/jwt.php at the ttl value.

Back to our API. The actual setting for boilerplate.signup_fields is

'signup_fields' => [
    'name', 'email', 'password'
],

… and it’s fine, considering our needs. Some basic rules are there, too:

'signup_fields_rules' => [
	'name' => 'required',
	'email' => 'required|email|unique:users',
	'password' => 'required|min:6'
],

So far, we haven’t had to do anything. Also, we already have our routes in the app/Http/api_routes.php file.

// app/Http/api_routes.php

$api = app('Dingo\Api\Routing\Router');

$api->version('v1', function ($api) {

	$api->post('auth/login', 'App\Api\V1\Controllers\AuthController@login');
	$api->post('auth/signup', 'App\Api\V1\Controllers\AuthController@signup');

	...

});

The router we’re using here is a little bit different from the normal one we see in Laravel. We are, in fact, using the custom Dingo API Router.

The user part is now complete without typing a single line of code.

The only one remaining part is the logout procedure. However, a RESTful API is stateless by definition and this is something we are going to implement in the client.

Now, we should at least test our two procedures, to be sure. The first thing to do is to migrate our database. In our terminal, let’s type

	php artisan migrate

Let’s test a basic signup procedure. We’ll open an HTTP Client and make a call to the signup route. For this article’s purposes, we will use Postman. Let’s fill our request body…

Request body filled out

… send it and wait for the result …

Result produced

Done! Our user is now added to the application. To be sure, let’s make another test. This time for the login.

Logging in via postman

Great! The login procedure also works fine! We don’t need anything else for the User part. Time to finish our API by implementing the Book part.

Building the Book Part

In order to implement the Book side, we will need a migration to define our schema structure, a model and, finally, a controller to handle all the operations. Let’s start with migrations. Let’s open the terminal and type

	php artisan make:migration create_books_table --create=books

and a new file will be created in database/migrations. We then add the following fields in the up() method.

public function up()
{
    Schema::create('books', function (Blueprint $table) {
        $table->increments('id');

		// adding specific fields here...
        $table->string('title');
        $table->string('author_name');
        $table->integer('pages_count');

        $table->integer('user_id')->index();

        $table->timestamps();
    });
}

Time to “migrate” again, by typing

	php artisan migrate

to add the new table to the schema. Now, let’s create the model, with

	php artisan make:model Book

and we are done. Due to the conventions in Laravel, the Book model already “knows” that it has to work with the books table we have created. Here’s the model:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $fillable = ['title', 'author_name', 'pages_count'];
}

We are going to put these fields (title, author_name and pages_count) into the fillable array in order to use the fill method on the update procedure (more on that soon).

Now, we have to edit our User model in order to define the relationship we will need to retrieve their related books. In app\User.php we add the following method:

public function books()
{
    return $this->hasMany('App\Book');
}

The last step is the resource controller for the Book entity. Let’s type

	php artisan make:controller BookController

to create a new resource controller. Laravel will create it in the app/Http/Controllers folder. We will move it to app/Api/V1/Controllers.

We also change the namespace:

namespace App\Api\V1\Controllers;

where App is the basic namespace we have chosen for our application. By default, we can leave it as it is.

Before proceeding, we have to remember to add some use directives at the beginning of the file:

use JWTAuth;
use App\Book;
use Dingo\Api\Routing\Helpers;

The first one is the JWTAuth Facade, which we will use to retrieve our user data from the token. The second one is our model. The third is a helper we will use to quickly create every response for outputting to the client in a RESTful manner.

We must also remember to include this Helpers trait in the controller:

...

class BookController extends Controller
{
    use Helpers;

...

Now we can implement our methods:

  • index, to get the books list of a certain user;
  • show, to get a single book given its id;
  • store, to store a newly created book in the database;
  • update, to store updates on a certain book, given its id;
  • destroy, to delete an existing book from the database, given its id;

Let’s start from the first: index().

public function index()
{
    $currentUser = JWTAuth::parseToken()->authenticate();
    return $currentUser
        ->books()
        ->orderBy('created_at', 'DESC')
        ->get()
        ->toArray();
}

This is really easy to understand. We are getting our user starting from the token, and then we are returning all the related books as output. Nothing more.

We can make a step forward, to store()

public function store(Request $request)
{
    $currentUser = JWTAuth::parseToken()->authenticate();

    $book = new Book;

    $book->title = $request->get('title');
    $book->author_name = $request->get('author_name');
    $book->pages_count = $request->get('pages_count');

    if($currentUser->books()->save($book))
        return $this->response->created();
    else
        return $this->response->error('could_not_create_book', 500);
}

Like before, we are getting the current user with the JWTAuth method, parseToken()->authenticate(). Then, we are creating our instance and saving the relationship with the save method.

If everything goes right, a Created 201 response will be returned. Otherwise, the client will get a custom 500 error. As we can see, it’s not just about the body: headers also will be tweaked accordingly to follow RESTful standards.

Let’s do some tests before finishing our controller. Add the following code into api_routes.php:

$api->group(['middleware' => 'api.auth'], function ($api) {
	$api->post('book/store', 'App\Api\V1\Controllers\BookController@store');
	$api->get('book', 'App\Api\V1\Controllers\BookController@index');
});

We have both routes now! Let’s get back to our HTTP Client and log in again. With our new token, let’s make some requests to store a new book and, right after, get a list of them.

We can pass the token in two ways: passing it as a simple request parameter, or in the header with this specific item: Authorization: Bearer {token_goes_here}.

That said, here’s an example of a store request and the subsequent index one.

Token filled out for storing

… and then …

Index lists out books

It works!

We can finish the job by implementing the three remaining methods: show, update and destroy.

Here they are!

...

public function show($id)
{
    $currentUser = JWTAuth::parseToken()->authenticate();

    $book = $currentUser->books()->find($id);

    if(!$book)
        throw new NotFoundHttpException; 

    return $book;
}

public function update(Request $request, $id)
{
    $currentUser = JWTAuth::parseToken()->authenticate();

    $book = $currentUser->books()->find($id);
    if(!$book)
        throw new NotFoundHttpException;

    $book->fill($request->all());

    if($book->save())
        return $this->response->noContent();
    else
        return $this->response->error('could_not_update_book', 500);
}

public function destroy($id)
{
    $currentUser = JWTAuth::parseToken()->authenticate();

    $book = $currentUser->books()->find($id);

    if(!$book)
        throw new NotFoundHttpException;

    if($book->delete())
        return $this->response->noContent();
    else
        return $this->response->error('could_not_delete_book', 500);
}

...

We continued to use the $currentUser as an implicit “safety check”. In real world cases you should build something more robust.

It’s also possible to throw some Symfony exceptions. The API will automatically recognize them and encode in the right output format. You can find the complete supported exceptions list here.

In show, update and destroy, we used the NotFoundHttpException to tell the client that we have not found a certain resource.

Now, all we have to do is to add the last three new routes to the api_routes.php file.

$api->group(['middleware' => 'api.auth'], function ($api) {
	$api->get('books', 'App\Api\V1\Controllers\BookController@index');
	$api->get('books/{id}', 'App\Api\V1\Controllers\BookController@show');
	$api->post('books', 'App\Api\V1\Controllers\BookController@store');
	$api->put('books/{id}', 'App\Api\V1\Controllers\BookController@update');
	$api->delete('books/{id}', 'App\Api\V1\Controllers\BookController@destroy');
});

Laravel (and Dingo) has the shortcut method resource() that we can use to bind all the routes we just saw with a single instruction.

$api->resource('books', 'App\Api\V1\Controllers\BookController');

Choose the approach you like the most.

The controller is now complete. Here it is in full!

Note: we could stumble upon some issues using PUT and DELETE verbs with some body data. If the body is always empty, try to use x-www-form-urlencoded instead of form-data. It should fix the problem.

The server part is done!

Conclusion

In this part, we focused on the back-end and used a fairly stable and easy to use boilerplate to build our API and authentication + registration flow. Just like that, our app is already client-friendly. Slap together an iOS or Android app or even one of the many JavaScript frameworks and you can consume the content of our application with ease. That’s exactly what we’ll be focusing on in part 2 – building an Angular app for this back end from scratch! Stay tuned!

  • Jon

    Nice article and work Francesco,

    This definitely speeds up the development of an API with Laravel.

    I’m missing an important feature though: token refresh when it expires.
    It should be included before every protected request to the API.

    Any idea on how this could be included gracefully?

    Cheers

  • Satyajit Dey

    What should i do , if i would like to change password field name like (user_pass)?

  • Satyajit Dey

    Thanks Francesco..

  • Satyajit Dey

    Can you help me , If I would like to pass token through header then what should i do?

  • Jon

    Hi Francesco,

    Just was not sure if it was the best practice to refresh the token with each request.
    For sure it id the most secure way but I was wondering that the clients that consume the API should be aware of that.

    Great work again.

    Cheers

    • http://hellofrancesco.com Francesco Malatesta

      Hi Jon,

      thank you for your comment! Well, if you decide to release the token after every request, of course you will have to notify your client in various ways, starting from the API docs if you’re exposing your APIs to the “external” world :)

      Cheers!

  • Taylor Hamling

    Hey, thanks for the guide. I’ve managed to setup user signup and login but I’m having issues with the reset and recover endpoints.

    It’s failing on:

    $response = Password::reset($credentials, function ($user, $password) {
    $user->password = $password;
    $user->save();
    });

    and

    $response = Password::sendResetLink($request->only(’email’), function (Message $message) {
    $message->subject(Config::get(‘boilerplate.recovery_email_subject’));
    });

    I end up getting a 500 error back even though all the field are there. Anything I might be missing?

  • http://www.innovaat.nl Innovaat

    I had to add a Hash to make it working. Otherwise, the password gets stored unencrypted in the database (yuk!). Add ‘use Hash;’ at the top of your Controller. And add the following line to the signup function just before the User::create line:
    $userData[‘password’] = Hash::make($userData[‘password’]);

  • Fcog

    Excellent tutorial! thank you! Looking forward for the frontend tutorial!

  • Gamer Gemparq

    Managed to figure out what went wrong.

    I used the Auth’s token when resetting, which is wrong. The correct token can be found after calling /recovery endpoint and is located in the password_resets folder.

    • Dominic

      should we need to be logged in to reset password?..I am trying to reset password when the user has forgotten his password..the params are registered email,passowrd ,password confirmation and token from password reset table..still i am getting password cannot reset error..any help

  • http://www.lolstone.com TheUnreal

    Awesome! but how do I get the user info from here using this jwt token?

  • ricky

    run first
    composer install –no-scripts

  • Shivam Sharma

    hello
    kindly provide some correction for passwrod reset and recovery method

  • Shivam Sharma

    When I try to update any book it gives me 204 no content error
    I provided a put request with the credentials token , title ,author_name,page_count

    Kindly help

  • Shivam Sharma

    {
    “error”: {
    “message”: “SQLSTATE[42S22]: Column not found: 1054 Unknown column ’email’ in ‘where clause’ (SQL: delete from `password_resets` where `email` = usha.iuac@gmail.com)”,
    “code”: “42S22”,
    “status_code”: 500
    }
    }
    when I try for recovery it gives me the above error ,
    password_reset doesn’t have any data
    How should I ensure that token and email should be there in password_reset table

    • Shivam Sharma

      Ok I coined out the answer

  • Shivam Sharma

    does the recovery query sends a mail ?

  • Eric Johnson

    Awesome tutorial! Hope to see more in the future.

  • jnuc093

    i added:

    $api->version(‘v1’, [‘middleware’ => ‘cors’],function ($api) {

    it works fine

  • jnuc093

    how can this project add a Socialite Providers.

  • Val

    Thanks for this great tutorials, and thanks for sharing on github. This has helped me a lot as a novice. I have a question;

    Is it possible to retrieve the details of a Logged in User immediately after they login?

    I tried something like this: $currentUser = app(‘api.auth’)->user();

    then $currentUser[‘user_id’]

    but I get null. I placed this in the same Login routine, I intend returning the Token and User Details when a user login.

    Thanks, hoping for a positive answer.

  • Chandra Kurniawan

    Hi, thanks for the great tutorial for building API with laravel and jwt. It works perfectly.
    I want to use this as a base web API for android app I’m developing ^^

  • http://barung-project.com Vinra Pandia

    Hi frans, how get token from header,
    because i see in your tutorial, token sent by url.

  • Josh Harington

    So what would be the best way of loading images?

  • Omar Faruk

    hey Francesco Malatesta Thanks for your nice artical. I am stack with book api. my authentication is ok. but problem is when i am trying to store a book with logged in auth token. I am passing token as you have shown in this tutorial in schreenshot. It’s giving me 404 not Found exception. And i am confused about the second optionAuthorization: Bearer {token_goes_here} “We can pass the token in two ways: passing it as a simple request parameter, or in the header with this specific item: Authorization: Bearer {token_goes_here}.”

    Can you help me why it’s not working when i am passing the token like

    http://books.dev/api/book/store?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIsImlzcyI6Imh0dHA6XC9cL2Jvb2tzLmRldlwvYXBpXC9hdXRoXC9sb2dpbiIsImlhdCI6MTQ2NzMwMjg1MywiZXhwIjoxNDY3MzA2NDUzLCJuYmYiOjE0NjczMDI4NTMsImp0aSI6ImNlN2RmMzM5ZmM3ZTNjY2Y2ODI5ZTdhYmFlZGZjYTM0In0._0SNm_QpWzFtmoMUk3zWVUuj7dD4VRwcCPKH3At8sXQ

    • http://hellofrancesco.com Francesco Malatesta

      Hi there Omar! If you’re getting a 404 error you’re probably made some errors when specifying your routes :) Also, the domain you linked here is a local stored hostname, not public. Cheers!

      • Omar Faruk

        Hey @Francesco Malatesta:disqus , thanks for reply. I have solved my problem. Another thing i would like to know. what will be the logout method for this api? like this Auth::guard($this->getGuard())->logout(); . i am also implementing this into an android application. So how can i call this api from android end? do i need to pass the auth token to call logout api? i have already done registration and login part in android. After login i have auth_token in my session of android app. and how can i get the current user info?

  • Emiliano Torres

    hi, i get an error {
    “message”: “405 Method Not Allowed”,
    “status_code”: 405
    }

    on POST. If i try with GET its work fine. Any ideas? Thx. (sorry for my inglish)

  • AndrewGalford90278

    my wife was wanting IRS 2220 several days ago and was told about an excellent service with a lot of fillable forms . If you are wanting IRS 2220 as well , here’s a http://goo.gl/bbvIh2

  • Joe Krump

    Thank you for the tutorial. I had been looking for something that covered JWT authentication in Laravel for a while. This was very helpful.

  • Randel

    Francesco, Thanks for the tutorial. Its been invaluable. Going forward do you have any plans of updating this boilerplate to laravel 5.3 and/or Dingos release candidate? That is after they are actually released, which they currently are not.

    If you do make the update are you thinking about a fresh 5.3 install or are you going to continue from 5.2. It looks like its already been upgraded from 5.1..

    Regardless of your plans. Thanks for the great repo and tutorial. You’ve made my life a lot easier.

  • Chris Saunders

    Interesting … All works well, except when i put the routes in the group and add the api.auth middleware, then everytime i request using postman i get ‘Unable to authenticate with invalid token’ … Any help?

  • Douglas Moreno

    hi ,when i try to store a book it returns me {
    “error”: “token_not_provided”
    } i’m using laravel 5.1 and apache

  • joel kithinji

    How can I add and read my custom claims?

  • http://otcsoft.com/ it9xpro

    if not using postman , so how using api in controller .

  • Calvin Kwan

    PHP Fatal error: Class ‘AppApiV1ControllersController’ not found in /Applications/XAMPP/xamppfiles/htdocs/laravel_api/app/Api/V1/Controllers/BookController.php on line 14

    [SymfonyComponentDebugExceptionFatalErrorException]
    Class ‘AppApiV1ControllersController’ not found

    May I ask why I would be having this error?

    • Gautham Ram

      change the namespace to point to the namespace AppApiV1Controllers; and add use AppHttpControllersController; after that

  • Punit Sharma

    When i try to signup it gives me the following error

    {
    “message”: “Class app\Api\V1\Controllers\AuthController does not exist”,
    “status_code”: 500
    }

    • Gautham Ram

      you have to add “use AppHttpControllersController;” since it is extending the controller class.

  • http://www.kydevolution.com.ng/ Abati “AceKYD” Adewale

    Thank you for this wonderful piece… I’ve used it on more than one project already… Would definitely love to follow on twitter and also contribute to the repo when i can… Kudos

  • maanshu

    Can any one explain me how to join three or four models in a single api?

  • Mohammad Selim Miah

    Nice tutorial. It helped me a lot setting up my first RESTful api. Thanks a lot!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.