Shreyansh Pandey
This tutorial is out of date and no longer maintained.
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.
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.
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.
This API is the Developer API for a startup that returns the birda spotted in a particular area.
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.
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
birdshttp://api.app.com/birds
(POST
) - to create a new birdhttp://api.app.com/birds/:id
(GET
) - to get a specific birdThis 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.
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.
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.
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
- 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.
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:
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.
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 }` );
} );
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.
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 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:
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:
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.
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!