Tutorial

Making a RESTful API with Hapi.js

Draft updated on Invalid Date
    Default avatar

    By Shreyansh Pandey

    Making a RESTful API with Hapi.js

    This tutorial is out of date and no longer maintained.

    Introduction

    As the century dawns, we see new technologies and architectures that companies, startups, and developers, alike, are using to power their next big application; some of these architectures have been so thoroughly battle-tested, that some companies are even scrapping old applications just so that they can implement this modern, new (and scalable) approach.

    One of these architectures is the Representational State Transfer or REST as the people call it. In this architecture, the server talks in terms of resources, and then uses HTTP verbs to perform actions on those resources. A quick detour of this architecture has been taken in the following section.

    With Node.js, it’s no doubt that it’s super-easy to implement a quick RESTful API and be up and running in no time; however, there are quite a few considerations before someone thinks of implementing a scalable, high-efficiency RESTful API.

    This article builds on my previous article on Hapi.js. In case you find this confusing, please refer to the Part 1 of this tutorial. Once you’re through that one, you can come here and complete the entire series.

    Why am I using Hapi.js?

    Honestly, I am a HUGE fan of the Hapi.js framework, and the Hapi.js community. Cheers to all those who have contributed to this remarkable project.

    Hapi.js makes things so simple, and so smooth to work with; all that without compromising on the reliability or efficiency of the framework. In case you haven’t read it yet, I have a small article here which, as mentioned earlier, covers just enough material to get your feet wet and kicking for this project.

    Goals

    Now, before we start our work on this API, let’s set a couple of goals. Here the word ‘goals’ is like the success criterion of your application. Since this is a project to help you get up to speed with Hapi.js, we’ll really add the features which are unique: display or implement some of Hapi.js’ philosophy or concepts. It’ll also trim the deadwood from our experimental code-base.

    Primary API Function

    This API is the Developer API for a startup that returns the birda spotted in a particular area.

    Logical Goals

    • All users can see every public birda in the database;
    • All users can login/out;
    • Registered users can create birds;
    • Registered users can edit birds provided they own that bird;
    • Registered users can delete the birds.

    I presume everything in this section is quite self-explanatory. However, if you are confused about something, feel free to drop us a comment in the comments section of this post.

    Introduction to the RESTful Architecture

    In a RESTfully-architectured web application, you let the HTTP verbs (like GET, POST, etc.) do the work of telling your application what to do. For this tutorial, we’ll have routes something like the following:

    • http://api.app.com/birds (GET) - to get a list of all the public birds
    • http://api.app.com/birds (POST) - to create a new bird
    • http://api.app.com/birds/:id (GET) - to get a specific bird

    This is something quite standard of any RESTful API. There is a pretty good post on Scotch about designing APIs with RESTful architecture in mind. Take a look.

    Getting Started

    With everything said, let’s dig right into it. To test the API, I will be using this fantastic application called Paw. Alternatively, you can use ARC or Postman App.

    Tools of Choice

    To create this awesome API, we’ll be using a couple of very interesting Node.js packages.

    Knex.js

    Knex is a very simple to use, yet incredibly powerful query builder for MySQL and a plethora of other RDBMS. We’ll use this to directly communicate with our Authentication and Data servers running MySQL.

    Hapi.js

    Hapi (pronounced “happy”) is a web framework for building web applications, APIs, and services. It’s extremely simple to get started with and extremely powerful at the same time. The problem arises when you have to write performant, maintainable code. You can take a look at my getting started tutorial to get started with it, and then this tutorial as a continuation.

    Alright, perfect. Now we understand the nuances of this application and we can begin coding.

    Setting up package.json

    Initialize a new package.json file with npm init in your root folder and then filling the values as required. The following is my package.json:

    {
      "name": "birdbase",
      "version": "1.0.0",
    }
    

    Now, let’s install mysql, jsonwebtoken, hapi-auth-jwt, and knex with

    1. npm i --save mysql jsonwebtoken hapi-auth-jwt knex

    Note that this configuration is based on the configuration of my previous article. I haven’t included everything and this is just a build-up on that. Be sure to follow that one before this.

    Now, our package.json looks something like this:

    {
      "name": "birdbase",
      "version": "1.0.0",
      "devDependencies": {
        "babel-core": "^6.20.0",
        "babel-preset-es2015": "^6.18.0"
      },
      "dependencies": {
        "hapi": "^16.0.1",
        "hapi-auth-jwt": "^4.0.0",
        "jsonwebtoken": "^7.2.1",
        "knex": "^0.12.6",
        "mysql": "^2.12.0"
      },
      "scripts": {
        "start": "node bootstrap.js"
      }
    }
    

    And the following is the directory structure:

    Perfect. Now, let’s configure Knex so we can begin working with it.

    Setting up Knex

    Knex is just brilliant. And we will see the reasons for that brilliance in just a minute. Start by installing the knex cli with sudo npm install -g knex. This tool allows us to programatically create a MySQL table structure and then execute it. Generally, we call this migrations.

    For the rest of the tutorial, the following is my MySQL configuration:

    MySQL Host: 192.168.33.10
    MySQL User: birdbase
    MySQL Pass: password
    MySQL DB Name: birdbase
    

    Create a knexfile.js in the root of the directory with the following content:

    module.exports = {
    
        development: {
    
            migrations: { tableName: 'knex_migrations' },
            seeds: { tableName: './seeds' },
    
            client: 'mysql',
            connection: {
    
                host: '192.168.33.10',
    
                user: 'birdbase',
                password: 'password',
    
                database: 'birdbase',
                charset: 'utf8',
    
            }
    
        }
    
    };
    

    And create a new folder called seeds in the root directory. The knexfile.js is used by the Knex CLI to perform SQL operations. The seeds directory will contain our seeds or initial data which we can use for testing. Trust me when I say this, having this at hand, greatly simplifies development as you already have the data you want to work with.

    The structure should look something like the following:

    Creating the Migrations

    Let’s create the actual migrations now. We’ll create two tables users and birds. The users table will contain the username, password, name, and email of the users; and the birds table will contain the listings of birds.

    Create a new migration with knex migrate:make Datastructure to create a new migration file. It’ll look something like 20161211185139_Datastructure.js. In your favorite text editor, open it and you’ll see something like:

    exports.up = function(knex, Promise) {
    };
    
    exports.down = function(knex, Promise) {
    };
    

    The up function is executed when you migrate a database for this, and the down function is executed when you roll back.

    Add the following to the up function:

    exports.up = function(knex, Promise) {
    
        return knex
                .schema
                .createTable( 'users', function( usersTable ) {
    
                    // Primary Key
                    usersTable.increments();
    
                    // Data
                    usersTable.string( 'name', 50 ).notNullable();
                    usersTable.string( 'username', 50 ).notNullable().unique();
                    usersTable.string( 'email', 250 ).notNullable().unique();
                    usersTable.string( 'password', 128 ).notNullable();
                    usersTable.string( 'guid', 50 ).notNullable().unique();
    
                    usersTable.timestamp( 'created_at' ).notNullable();
    
                } )
    
                .createTable( 'birds', function( birdsTable ) {
    
                    // Primary Key
                    birdsTable.increments();
                    birdsTable.string( 'owner', 36 ).references( 'guid' ).inTable( 'users' );
    
                    // Data
                    // Each chainable method creates a column of the given type with the chained constraints. For example, in the line below, we create a column named `name` which has a maximum length of 250 characters, is of type string (VARCHAR) and is not nullable.
                    birdsTable.string( 'name', 250 ).notNullable();
                    birdsTable.string( 'species', 250 ).notNullable();
                    birdsTable.string( 'picture_url', 250 ).notNullable();
                    birdsTable.string( 'guid', 36 ).notNullable().unique();
                    birdsTable.boolean( 'isPublic' ).notNullable().defaultTo( true );
    
                    birdsTable.timestamp( 'created_at' ).notNullable();
    
                } );
    
    };
    

    The chain .references(...) is used to create a composite primary key. This is done to ensure that we know who owns which listing.

    In the down function, add the following:

    exports.down = function(knex, Promise) {
    
        // We use `...ifExists` because we're not sure if the table's there. Honestly, this is just a safety measure.
        return knex
            .schema
                .dropTableIfExists( 'birds' )
                .dropTableIfExists( 'users' );
    
    };
    

    Remember to drop a referencing table first; i.e., a table that uses field-referencing. In this case, birds had referenced guid in the table users, and so, we remove birds before we remove users as doing it the other way round will throw an error.

    Let’s run this migration with knex migrate:latest.

    If all goes well, you should see:

    Now, if you head over to phpMyAdmin and check your database, you should see something like the following:

    Looks great to me. We’ll now create some seed files by running knex seed:make 01_Users. Remember that the seed files are executed in the order of their file name, and so, if you execute the seed for the birds table first, the key constraint (reference) to guid in user will fail because the latter doesn’t exist.

    Under the seed folder, you should now see a new file titled 01_Users.js; open it, and replace the code with the following:

    exports.seed = function seed( knex, Promise ) {
    
        var tableName = 'users';
    
        var rows = [
    
            // You are free to add as many rows as you feel like in this array. Make sure that they're an object containing the following fields:
            {
                name: 'Shreyansh Pandey',
                username: 'example',
                password: 'password',
                email: 'example@example.com',
                guid: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
            },
    
        ];
    
        return knex( tableName )
            // Empty the table (DELETE)
            .del()
            .then( function() {
                return knex.insert( rows ).into( tableName );
            });
    
    };
    

    The code is self-explanatory, so I wouldn’t bother going deeper. Similarly, let’s create a sample bird migration with: knex seed:make 02_Birds and replacing the file with the following code:

    exports.seed = function seed( knex, Promise ) {
    
        var tableName = 'birds';
    
        var rows = [
    
            {
                owner: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
                species: 'Columbidae',
                name: 'Pigeon',
                picture_url: 'pigeon.jpg',
                guid: '4c8d84f1-9e41-4e78-a254-0a5680cd19d5',
                isPublic: true,
            },
    
            {
                owner: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
                species: 'Zenaida',
                name: 'Mourning dove',
                picture_url: 'mourning_dove.jpg',
                guid: 'ddb8a136-6df4-4cf3-98c6-d29b9da4fbc6',
                isPublic: false,
            },
    
        ];
    
        return knex( tableName )
            .del()
            .then( function() {
                return knex.insert( rows ).into( tableName );
            });
    
    };
    

    Execute these seeds with knex seed:run and then open phpMyAdmin to be amazed.

    Beautiful. Now we can move onto creating the actual API.

    Starting the API

    JWT Authentication

    The authentication provider. In this case, we’ll be using the super-simple and secure JSON Web Token strategy for authentication and authorization. Before moving forward, we need to do JWT-101.

    A JWT is in the form xxxxx.yyyyy.zzzzz with each of the sections having a specific name. We’ll consider the token:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
    eyJ1c2VybmFtZSI6ImxhYnN2aXN1YWwiLCJzY29wZSI6ImFkbWluIiwiaWF0IjoxNDgxMzg0NDQ3LCJleHAiOjE0ODEzODgwNDd9.
    Y7B8rvGNmkwrSWMlb5e1Bqz0qnLuDLxerZmmdtg8ouo
    

    The first block (xxxxx) is the header of the token and contains metadata such as the algorithm used for signature, etc. In our token, on decoding with UTF8 encoding, we get the following content

    {
      "alg":"HS256",
      "typ":"JWT"
    }
    

    The next block (yyyyy) is the payload of the token and contains claims, expiration and creation metadata. Technically speaking, you can have whatever you want in this section. For our example, we get the following decoded content

    {
      "username": "example",
      "scope": "admin",
      "iat": 1481384447,
      "exp": 1481388047
    }
    

    As you can make out, this token was created for the user example who has admin scope and expires in 1h. Simple.

    The last block (zzzzz) is the signature of the token and is calculated using HMAC256( ( base64Encode( header ) + '.' + base64Encode( payload ) ), secretKey ). You define the secret on the server; the library (jsonwebtoken) signs and verifies the tokens using this secret. No matter what happens, make sure you do not leak this token anywhere as it will cause a problem in the authentication framework of your application.

    A great tool to interactively debug and learn about JWT is its official website at JWT and the token debugger here. Below is a screenshot of the interactive JWT debugger in action.

    Setting Up JWT

    Before we do anything, we need to tell hapi that we’re going to use an authentication strategy (method) and so, it should load a couple of modules. Open up server.js and enter the following just after server.connection(...

    // .register(...) registers a module within the instance of the API. The callback is then used to tell that the loaded module will be used as an authentication strategy.
    server.register( require( 'hapi-auth-jwt' ), ( err ) => {
        server.auth.strategy( 'token', 'jwt', {
    
            key: 'YOUR_PRIVATE_KEY',
    
            verifyOptions: {
                algorithms: [ 'HS256' ],
            }
    
        } );
    
    } );
    

    Here, we ask the server object to register a new module from the package hapi-jwt-auth; afterwhich, we register a new authentication strategy called token with the jwt scheme and the following options:

    • key - this is the private key that is used to sign and verify the JWT signatures;
    • verifyOptions - we tell the library which algorithm to use for signature and verification; HMAC256 in this case.

    You can also add validateFunc to the block which is used to validate the token provided. This is optional and is used when you have to do some other sort of verification in addition to the cryptographic verification provided by the library.

    src/server.js should look like the following now

    import Hapi from 'hapi';
    
    const server = new Hapi.Server();
    
    server.connection( {
        port: 8080
    } );
    
    server.register( require( 'hapi-auth-jwt' ), ( err ) => {
        server.auth.strategy( 'token', 'jwt', {
    
            key: 'YOUR_PRIVATE_KEY',
    
            verifyOptions: {
                algorithms: [ 'HS256' ],
            }
    
        } );
    
    } );
    
    server.start( err => {
    
        if( err ) {
    
            // Fancy error handling here
            console.error( 'Error was handled!' );
            console.error( err );
    
        }
    
        console.log( `Server started at ${ server.info.uri }` );
    
    } );
    

    Routes

    Now, we can add the routes. Let’s start by adding a simple route which gets all the public birds. Within src/server.js add the following route:

    server.route( {
    
        path: '/birds',
        method: 'GET',
        handler: ( request, reply ) => {
        }
    
    } );
    

    Now, let’s create a knex instance. Add a file knex.js within src and add the following code to it:

    export default require( 'knex' )( {
    
        client: 'mysql',
        connection: {
    
            host: '192.168.33.10',
    
            user: 'birdbase',
            password: 'password',
    
            database: 'birdbase',
            charset: 'utf8',
    
        }
    
    } );
    

    Then import this file into your server.js by import Knex from './knex';. Now we are ready to utilize this awesome library.

    Let’s select the name, picture_url and species for every public bird. In the handler for your route, add:

    ...
    handler: ( request, reply ) => {
    
        // In general, the Knex operation is like Knex('TABLE_NAME').where(...).chainable(...).then(...)
        const getOperation = Knex( 'birds' ).where( {
    
            isPublic: true
    
        } ).select( 'name', 'species', 'picture_url' ).then( ( results ) => {
    
            if( !results || results.length === 0 ) {
    
                reply( {
    
                    error: true,
                    errMessage: 'no public bird found',
    
                } );
    
            }
    
            reply( {
    
                dataCount: results.length,
                data: results,
    
            } );
    
        } ).catch( ( err ) => {
    
            reply( 'server-side error' );
    
        } );
    
    }
    ...
    

    The line = Knex('... tells Knex to use the birds database, and then builds the query where the field isPublic is set to true. Then fetches it using .select (which returns a promise) and then resolving the promise. The parameter results is an array of all the birds which match the criterion.

    Save the file and start the API server with npm start and then fire up your favorite API client. We’ll use Paw.

    Pat yourself on the back if everything works as expected. Your src/server.js file should look like this:

    import Hapi from 'hapi';
    import Knex from './knex';
    
    const server = new Hapi.Server();
    
    server.connection( {
        port: 8080
    } );
    
    server.register( require( 'hapi-auth-jwt' ), ( err ) => {
        server.auth.strategy( 'token', 'jwt', {
    
            key: 'YOUR_PRIVATE_KEY',
    
            verifyOptions: {
                algorithms: [ 'HS256' ],
            }
    
        } );
    
    } );
    
    // --------------
    // Routes
    // --------------
    
    server.route( {
    
        path: '/birds',
        method: 'GET',
        handler: ( request, reply ) => {
    
            const getOperation = Knex( 'birds' ).where( {
    
                isPublic: true
    
            } ).select( 'name', 'species', 'picture_url' ).then( ( results ) => {
    
                // The second one is just a redundant check, but let's be sure of everything.
                if( !results || results.length === 0 ) {
    
                    reply( {
    
                        error: true,
                        errMessage: 'no public bird found',
    
                    } );
    
                }
    
                reply( {
    
                    dataCount: results.length,
                    data: results,
    
                } );
    
            } ).catch( ( err ) => {
    
                reply( 'server-side error' );
    
            } );
    
        }
    
    } );
    
    server.start( err => {
    
        if( err ) {
    
            // Fancy error handling here
            console.error( 'Error was handled!' );
            console.error( err );
    
        }
    
        console.log( `Server started at ${ server.info.uri }` );
    
    } );
    

    Now, we’ll continue by adding an auth route which will be used to authenticate the user. The logic here is very simple: check if the password of the payload is the same as the one in the database, and if so, create a new JWT token with the scope of the user’s GUID which expires in 1h.

    While updating a bird, we’ll use a preRouteHandler to check if the current user owns the bird; if he does, then we’ll allow the edit, otherwise, we’ll throw a 403 error.

    Auth Route

    Let’s create a POST route with

    server.route( {
    
        path: '/auth',
        method: 'POST',
        handler: ( request, reply ) => {
    
            // This is a ES6 standard
            const { username, password } = request.payload;
    
            const getOperation = Knex( 'users' ).where( {
    
                // Equiv. to `username: username`
                username,
    
            } ).select( 'guid', 'password' ).then( ( results ) => {
    
            } ).catch( ( err ) => {
    
                reply( 'server-side error' );
    
            } );
    
        }
    
    } );
    

    The line const { username... decomposes the request.payload object and gets the named values (username and password in this case). This is the same as:

    const username = request.payload.username;
    

    Just another lovely example of why I love ES6.

    Remember that the object request.payload contains all the content in a POST or a PUT request.

    Let’s continue.

    First we need to make sure that we select exactly one. Let’s use the array deconstructor and then check if it’s populated:

    ...
    } ).select( 'guid', 'password' ).then( ( [ user ] ) => {
        if( !user ) {
    
            reply( {
    
                error: true,
                errMessage: 'the specified user was not found',
    
            } );
    
            // Force of habit. But most importantly, we don't want to wrap everything else in an `else` block; better is, just return the control.
            return;
    
        }
    ...
    

    Simple enough. We check if the user exists and if not, we throw an error and exit out of the function. Let’s finish this route:

    ...
    // Honestly, this is VERY insecure. Use some salted-hashing algorithm and then compare it.
    if( user.password === password ) {
    
        const token = jwt.sign( {
    
            // You can have anything you want here. ANYTHING. As we'll see in a bit, this decoded token is passed onto a request handler.
            username,
            scope: user.guid,
    
        }, 'YOUR_PRIVATE_KEY', {
    
            algorithm: 'HS256',
            expiresIn: '1h',
    
        } );
    
        reply( {
    
            token,
            scope: user.guid,
    
        } );
    
    } else {
    
        reply( 'incorrect password' );
    
    }
    

    The function jwt.sign( payload, key, [ options ] ) signs the payload and gives a JWT which we then transmit to the user. Save this file and start your server, and add a new request to your client.

    Try changing the username and the password to see what kind of response you get.

    So far, so good. Now we’ll add a method to create a bird, and a method to update a bird. Let’s start.

    Create a Bird

    Create a POST route at /birds and add the empty handler function.

    server.route( {
    
        path: '/birds',
        method: 'POST',
        handler: ( request, reply ) => {
    
            const { bird } = request.payload;
    
        }
    
    } );
    

    We expect to have a payload as bird which is an object containing all the information about the bird. Let’s add the code to insert this into our database.

    Before anything, we need to tell Hapi.js that this route is protected by authentication. To do so, add the following after method in the route:

    ...
    method: 'POST',
    config: {
    
        auth: {
    
            strategy: 'token',
    
        }
    
    },
    ...
    

    This tells Hapi.js that we’ll be using a registered authentication strategy for our route.

    But, for this to work properly, we need to refractor the code a little bit. Let’s add a new file routes.js containing all the routes. Something like:

    import Knex from './knex';
    import jwt from 'jsonwebtoken';
    
    // The idea here is simple: export an array which can be then iterated over and each route can be attached.
    const routes = [
    
        {
    
            path: '/birds',
            method: 'GET',
            handler: ( request, reply ) => {
    
                const getOperation = Knex( 'birds' ).where( {
    
                    isPublic: true
    ...
    export default routes;
    

    Then, in src/server.js, we’ll import the routes array as import routes from './routes'; and then within server.register(... we’ll add the following bit to register all the routes:

    ...
        routes.forEach( ( route ) => {
    
            console.log( `attaching ${ route.path }` );
            server.route( route );
    
        } );
    

    The src/server.js file becomes something like:

    import Hapi from 'hapi';
    import routes from './routes';
    
    const server = new Hapi.Server();
    
    server.connection( {
        port: 8080
    } );
    
    server.register( require( 'hapi-auth-jwt' ), ( err ) => {
    
        if( !err ) {
            console.log( 'registered authentication provider' );
        }
    
        server.auth.strategy( 'token', 'jwt', {
    
            key: 'YOUR_PRIVATE_KEY',
    
            verifyOptions: {
                algorithms: [ 'HS256' ]
            }
    
        } );
    
        // We move this in the callback because we want to make sure that the authentication module has loaded before we attach the routes. It will throw an error, otherwise.
        routes.forEach( ( route ) => {
    
            console.log( `attaching ${ route.path }` );
            server.route( route );
    
        } );
    
    } );
    
    server.start( err => {
    
        if( err ) {
    
            // Fancy error handling here
            console.error( 'Error was handled!' );
            console.error( err );
    
        }
    
        console.log( `Server started at ${ server.info.uri }` );
    
    } );
    

    With that done, let’s move on.

    Now, we need to add a bird to the birds database. We’ll do this using Knex. Add the following bit immediately after const { bird } = request.payload; in your route within src/routes.js.

    We’ll install a package to generate GUID’s called node-uuid with npm i --save node-uuid and then import it into our routes file as import GUID from 'node-uuid';.

    const guid = GUID.v4();
    
    const insertOperation = Knex( 'birds' ).insert( {
    
        owner: request.auth.credentials.scope,
        name: bird.name,
        species: bird.species,
        picture_url: bird.picture_url,
        guid,
    
    } ).then( ( res ) => {
    
        reply( {
    
            data: guid,
            message: 'successfully created bird'
    
        } );
    
    } ).catch( ( err ) => {
    
        reply( 'server-side error' );
    
    } );
    

    The code is pretty self-explanatory apart from this interesting object request.auth.credentials. Well, after the verification is done, the authentication handler passes on the decoded token to this credentials object. If you do a console.log( request.auth.credentials ); you’ll see something like:

    {
        username: 'example',
        scope: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
        iat: 1481546651,
        exp: 1481550251
    }
    

    From here, we can grab on to the GUID for the user and pass it on as the owner in the database. Simple.

    Fire up the server and then add a couple of requests. Remember to add the Authorization header in the following format: Authorization: Bearer <JWT> where <JWT> is your generated JWT in the /auth route.

    Let’s check the database:

    And now let’s get a listing of all public birds:

    Update Birds

    With that in place, we can create our last route: PUT at /birds/:guid where we can update the bird with the GUID guid. I’ll just copy and paste the POST route and make the changes as and when required.

    For starters, let’s change the method to PUT and the route to /bird/{birdGuid}. We can access this birdGuid from request.params. After the changes, it should look something like:

    {
    
            path: '/birds/{birdGuid}',
            method: 'PUT',
    ...
    

    Now, we want to verify that the current user has rights to the bird he’s trying to edit. For that, we need to validate if the bird associated with birdGuid has the same owner as the scope of the authorization token passed. As illustrated before, we can access the token’s GUID from request.auth.credentials and then the scope property in that. However, let’s add a route prerequisite: this function has the same signature as the handler of a route and is executed before the control is passed to the handler. Really useful in cases like this where you need to do some sort of verification. Also note that we can safely assume that when this function is being executed, some user has passed some valid JWT in the authorization header; had they not done that, the hapi-jwt-auth library would’ve thrown a 401 error.

    The route should be something like:

    config: {
        ...
        pre: [
            {
                method: ( request, reply ) => {
    
                }
            }
        ]
    ...
    

    The pre configuration block is an array which contains objects with a key method which is linked to a function.

    We’ll first pull out all the values from the objects and store them:

    ...
    const { birdGuid } = request.params
             , { scope }    = request.auth.credentials;
    ...
    

    Next, let’s do a select operation on the database where we select the bird from the GUID provided; we’ll just select the owner column of the bird as that’s all we need for verification:

    ...
    const getOperation = Knex( 'birds' ).where( {
    
        guid: birdGuid,
    
    } ).select( 'owner' ).then( ( [ result ] ) => {
    
    } );
    ...
    

    Brilliant. Now, we have selected just one bird with [ result ] and we can work on that.

    Let’s start by verifying that we actually have a bird with the specified GUID; if we do not, then we’ll take over the request and send a custom reply. reply().takeover() ends the reply chain with the last response you give, and hence, does not let the handler get envoked.

    ...
    if( !result ) {
    
        reply( {
    
            error: true,
            errMessage: `the bird with id ${ birdGuid } was not found`
    
        } ).takeover();
    
    }
    ...
    

    Next, let’s check if the scope of the current token allows the user to modify the bird with the guid.

    ...
    if( result.owner !== scope ) {
    
        reply( {
    
            error: true,
            errMessage: `the bird with id ${ birdGuid } is not in the current scope`
    
        } ).takeover();
    
    }
    ...
    

    If not, we’ll just let the reply chain continue:

    ...
    return reply.continue();
    ...
    

    After you’re done, this method should be something like:

    ...
    method: ( request, reply ) => {
    
        const { birdGuid } = request.params
            , { scope }    = request.auth.credentials;
    
        const getOperation = Knex( 'birds' ).where( {
    
            guid: birdGuid,
    
        } ).select( 'owner' ).then( ( [ result ] ) => {
    
            if( !result ) {
    
                reply( {
    
                    error: true,
                    errMessage: `the bird with id ${ birdGuid } was not found`
    
                } ).takeover();
    
            }
    
            if( result.owner !== scope ) {
    
                reply( {
    
                    error: true,
                    errMessage: `the bird with id ${ birdGuid } is not in the current scope`
    
                } ).takeover();
    
            }
    
            return reply.continue();
    
        } );
    
    }
    ...
    

    Lastly, let’s add the update chain to the handler so we can UPDATE the current bird with the information.

    ...
    handler: ( request, reply ) => {
    
        const { birdGuid } = request.params
            , { bird }     = request.payload;
    
        const insertOperation = Knex( 'birds' ).where( {
    
            guid: birdGuid,
    
        } ).update( {
    
            name: bird.name,
            species: bird.species,
            picture_url: bird.picture_url,
            isPublic: bird.isPublic,
    
        } ).then( ( res ) => {
    
            reply( {
    
                message: 'successfully updated bird'
    
            } );
    
        } ).catch( ( err ) => {
    
            reply( 'server-side error' );
    
        } );
    
    }
    ...
    

    The code is self-explanatory. So, we’ll just continue. Now, fire up the server and add a new request.

    Let’s change the isPublic parameter:

    You can also try giving incorrect birdGuid and seeing what happens:

    Conclusion

    In the end, we learned quite a few things here. We went from just being a beginner in Hapi.js to creating a fully blown API in Hapi with Authentication, MySQL DB, etc. The code is available here; if you find some errors, or have some suggestions, please be sure to tell me. if you have any questions, be sure to throw them in the comment box below. Until next time! Cheers!

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about us


    About the authors
    Default avatar
    Shreyansh Pandey

    author

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Get our biweekly newsletter

    Sign up for Infrastructure as a Newsletter.

    Hollie's Hub for Good

    Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

    Become a contributor

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    Welcome to the developer cloud

    DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

    Learn more
    DigitalOcean Cloud Control Panel