Michele Titolo | Blog | Speaking

Building APIs in Hapi and Node.js

Controllers

This is where it’s going to get interesting. I really liked how Padrino combined routes and logic in the same place, so I attempted to do so with Hapi. After combing through the boilerplate projects, I found the magic to allow me to split up my routes into separate controllers. It’s a simple module.exports = server; in my main index.js file.

An interlude on module.exports

Admittedly, before I sat down and started working on this test I dug a bit into how Node.js works. In the tutorials and sample code I looked through, there were always these calls to module.exports. It’s a documented feature of Node, and represents what is returned when a file or set of files is required. This can be a function or a class. But this does explain why at the end of all my Model files, there ends up being a return User; for instance.

// user.js
module.exports = function(sequelize, DataTypes) {
  var User = ...;
  return User;
};

// index.js
var models = require('./models');

server.route({
  method: 'GET',
  path: '/api',
  handler: function(request, reply) {
    models.User.findAll()
      .then(function(users) {
        reply(users).code(200);
      });
  }
});

Now it can be called whatever, but this way allows normalized access throughout the application. It’s still not a type system, but at least there is pseudo-namespacing.

The GET endpoints

I’m going to start by creating the simple routes for GET /users and GET /users/:id. The tutorial I followed had me essentially create /users but I didn’t have any data in my database, so it always returned an empty array []. This is already better than some of the other frameworks I tried out, as that is the expected behavior.

Seed data

In order to test all these out, I first need data. I don’t want to go through the process of creating all the endpoints and then using them to add data. Equalize has the ability to seed a database, so that is what I used. The CLI will create a seed file for you, and store it in ./seeds. But it is empty.

The documentation for what goes in a seed file is also pitiful. It took copious amounts of searching to find the right format for the up and down actions. For some reason up refuses to work without returning an array. Over the course of about 5 hours, I managed to get seeders to run just once.There’s also no reset or drop command, so if something went awry I had to manually fix the db. That was a feature requested in 2014 but nothing has come of it. It took me filing a bug, and actually finding bugs in the cli library (hint: don’t use JSON seederStorage) for this to start working again. Final seed file:

'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.bulkInsert('Users', [
        {email: "aphrodite@olympus.org", name: "Aphrodite",  createdAt: Date.now(), updatedAt: Date.now() },
        {email: "athena@olympus.org", name: "Athena", createdAt: Date.now(), updatedAt: Date.now() },
        {email: "zeus@olympus.org", name: "Zeus", createdAt: Date.now(), updatedAt: Date.now() },
        {email: "apollo@olympus.org", name: "Apollo", createdAt: Date.now(), updatedAt: Date.now() }
        ],{}
      );
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.bulkDelete({tableName: 'Users'}, null, {});
  }
};

I sunk at least 8 hours into getting this working, over the course of 3 nights. If you think I’m exaggerating, I’m not.

Writing tests

I don’t usually do TDD, but for this I’m going to use a test harness to enforce the contracts I set out in my Swagger document. This also is a super easy way to verify that as I change things, I don’t regress any endpoints. I’m not working on a full unit test suite, because the logic is simple. An integration suite covers all I need.

For this testing I decided to use Frisby.js. It runs on top of Jasmine, and spins up a server to serve requests and validate responses. It’s exactly what I need. My first test looks like this:

var frisby = require('frisby');

frisby.create('Ensure we get a list of users')
	.get('http://localhost:3000/users')
	.expectStatus(200)
	.expectHeaderContains('content-type', 'application/json')
	.expectJSONTypes('*', {
		name: String,
		id: Number,
		email: String
	})
	.toss();

This was also a struggle to get working, since there have been bugs fixed on the master branch, and no release has been made. It’s really frustrating to see abandonware, especially when so much work was done after the last release. After a lot of headaches I used the gulp-jasmine-phantom package instead of gulp-jasmine and managed to get it all working. All told getting the test harness setup was at least 5 hours of work, most of it debugging frisby and gulp.

Creating Controllers

The next part involves moving around some code, because I don’t want everything in the index.js file. Most of this worked as expected, and very little had to change to get it to be happy.

.
├── ./Gulpfile.js
├── ./app
│   ├── ./controllers
│   │   ├── ./index.js
│   │   └── ./user.js
│   └── ./models
│       ├── ./group.js
│       ├── ./index.js
│       ├── ./membership.js
│       └── ./user.js
├── ./db
│   ├── ./config
│   │   └── ./config.json
│   ├── ./db.development.sqlite
│   ├── ./migrations
│   │   ├── ./20160511001612-create-user.js
│   │   ├── ./20160511001808-create-group.js
│   │   └── ./20160511003357-create-membership.js
│   └── ./seeders
│       └── ./d20160511015238-users-and-groups.js
├── ./index.js
├── ./package.json
└── ./spec
    └── ./user_spec.js

The most challenging part of creating controllers was loading them. Since each javascript file exists in a vacuum, they need to be require’d in order to be used in other files. I could manually require every controller, but that would just get tedious. Thankfully this is a problem many have solved. After a lot of playing around, I ended up using the solution in this blog post, with a little modification for Hapi. It works quite well.

I thought about adding a prototype and actual controller objects, but automating the importing of separate methods used for different requests seemed like a pain, and I’ll be breaking standard REST conventions. I don’t want to dig myself into a hole. Each of my controllers module.exports one function that takes a Hapi server instance, and then assigns the routes. I can still create functions within the files to handle more complex logic, but most of these endpoints aren’t complicated.

JSON Templates

Compared to everything else in this blog post, this took the least amount of time. It helps that the library is new, hence the maintainer is still paying attention to it. The syntax is strange, as the same word ends up being repeated several times, i.e. reply.view('users', { users: users }). After a little oddness, everything was up and running smoothly. I created a partial for the user object, and then used that for both the list and the detail calls.

Failing the test

Over the past two weeks I’ve put in over 20 hours of work to get two endpoints working (I stopped after GET /users and GET /users/:id). I filed a few issues on OSS projects and made one post on Stack Overflow. While the remainder of the endpoints would hopefully be trivial at this point, I am very disappointed by the tooling. There’s going to be a learning curve with a new language and the popular libraries in it, but 20 hours to get 2 API endpoints working is ridiculous. I came to a conclusion: I don’t want to do this project in Javascript. I’ve already lost too much sleep and been frustrated to the point of wanting to give up (which I actually did one night). I don’t know where I’ll go from here, but hopefully some space away from the horrors of Javascript will give me some insight.

© 2023 Michele Titolo