Chapter 6. Serving Our First HTML Document

When creating an isomorphic JavaScript framework or application, most people begin with the client and then attempt to fit their solution to the server. This is likely because they began with a client-side application and later realized that they needed some of the benefits that an isomorphic application provides, such as an optimized page load. The problem with this approach is that client implementations are typically deeply linked to the browser environment, which makes transferring the application to the server a complicated process. This is not to say that starting from the server makes us impervious to environment-specific issues, but it does ensure that we begin from a request/reply lifecycle mindset, which is required by the server. And, the real advantage we have is that we do not have an investment in an existing code base, so we get to start with a clean slate!

Serving an HTML Template

Before we build any abstractions or define an API, let’s start with serving an HTML document based on a template, so we can familiarize ourselves with the server request/reply lifecycle. For this example and the rest that follow we will be using Nunjucks by Mozilla, which you can install as follows:

$ npm install nunjucks –save

Now that we have Nunjucks installed we can create a template, src/index.html, with the contents shown in Example 6-1.

Example 6-1. Nunjucks HTML document template
<head>
  <meta charset="utf-8">
  <title>
    And the man in the suit has just bought a new car
    From the profit he's made on your dreams
  </title>
</head>
<body>
  <p>hello {{fname}} {{lname}}</p>
</body>
</html>

Nunjucks uses double curly braces to render variables from the template context. Next, let’s modify ./src/index.js to return the compiled template with a first and last name when a user opens localhost:8000/hello+\ in the browser. Edit the file so it matches the version shown in Example 6-2.

Example 6-2. Serving an HTML document (./src/index.js)
import Hapi from 'hapi';
import nunjucks from 'nunjucks';

// configure nunjucks to read from the dist directory
nunjucks.configure('./dist');

// create a server with a host and port
const server = new Hapi.Server();
server.connection({
  host: 'localhost',
  port: 8000
});

// add the route
server.route({
  method: 'GET',
  path:'/hello',
  handler: function (request, reply) {
    // read template and compile using context object
    nunjucks.render('index.html', {
      fname: 'Rick', lname: 'Sanchez'
    }, function (err, html) {
      // reply with HTML response
      reply(html);
    });
  }
});

// start the server
server.start();

We made quite a few changes to our example from Chapter 5. Let’s break these down item by item and discuss them. The first thing we did was import nunjucks. Next, we configured nunjucks to read from the ./dist directory. Finally, we used nunjucks to read our template, ./dist/index.html, compile it using our context variable { fname: Rick, lname: Sanchez }, and then return an HTML string as the reply from our server.

This code looks great, but if you run gulp in the terminal and try to open localhost:8000/hello in a browser it will return an empty <body>. So why doesn’t it work? If you remember, we created our template in ./src, but we configured nunjucks to read from ./dist (which is what we want because ./dist contains our application distribution and ./src contains our application source). So how do we fix this? We need to update our build to copy our template. Modify gulpfile.js, adding a copy task and editing the watch task and the default task as shown in Example 6-3.

Example 6-3. Copying the template from source to distribution (gulpfile.js)
// previous code omitted for brevity

gulp.task('copy', function () {
  return gulp.src('src/**/*.html')
    .pipe(gulp.dest('dist'));
});

gulp.task('watch', function () {
  gulp.watch('src/**/*.js', ['compile']);
  gulp.watch('src/**/*.html', ['copy']);
});

// previous code omitted for brevity

gulp.task('default', function (callback) {
  sequence(['compile', 'watch', 'copy'], 'start', callback);
});

Now if we run gulp in the terminal and open the browser to localhost:8000/hello we should see “hello Rick Sanchez”. Success!

This is pretty cool, but not very dynamic. What if we want to change the first and the last name in the body response? We need to pass parameters to the server, similar (conceptually) to the way we pass arguments to a function.

Working with Path and Query Parameters

Often, an application needs to serve dynamic content. Path and query parameters, and sometimes a session cookie, drive the selection of this content by the server. In the previous section the first and last name values for our hello message were hardcoded in the route handler, but there is no reason why the values could not be determined by path parameters. In order to pass path parameters to our route handler, we need to update the path property of our route as shown in Example 6-4.

Example 6-4. Adding path parameters in ./src/index.js
server.route({
  method: 'GET',
  path:'/hello/{fname}/{lname}',
  handler: function (request, reply) {
    // read template and compile using context object
    nunjucks.render('index.html', {
      fname: 'Rick', lname: 'Sanchez'
    }, function (err, html) {
      // reply with HTML response
      reply(html);
    });
  }
});

With this change, the route will now match URIs like localhost:8000/hello/morty/smith and localhost:8000/hello/jerry/smith. The new values in these URI paths are path parameters and will be part of the request object: request.params.fname and request.params.lname. These values can then be passed to the template context, as shown in Example 6-5.

Example 6-5. Accessing path parameters
server.route({
  method: 'GET',
  path:'/hello/{fname}/{lname}',
  handler: function (request, reply) {
    // read template and compile using context object
    nunjucks.render('index.html', {
      fname: request.params.fname,
      laname: request.params.lname
    }, function (err, html) {
      // reply with HTML response
      reply(html);
    });
  }
});

If you load localhost:8000/hello/jerry/smith in your browser you should now see path parameters in the response body.

Another way to pass values via the URI is using query parameters, as in localhost:8000/hello?fname=morty&lname=smith and localhost:8000/hello?fname=jerry&lname=smith. To enable the use of query parameters, change the route to match Example 6-6.

Example 6-6. Accessing query parameters
server.route({
  method: 'GET',
  path:'/hello',
  handler: function (request, reply) {
    // read template and compile using context object
    nunjucks.render('index.html', {
      fname: request.query.fname,
      laname: request.query.lname
    }, function (err, html) {
      // reply with HTML response
      reply(html);
    });
  }
});

Those are two different ways for getting dynamic values to a route that can be used to drive the selection of dynamic content. You can also use both of these options and provide sensible defaults to create a more flexible route handler, as shown in Example 6-7.

Example 6-7. Accessing path and query parameters
function getName(request) {
  // default values
  let name = {
    fname: 'Rick',
    lname: 'Sanchez'
  };
  // split path params
  let nameParts = request.params.name ? request.params.name.split('/') : [];

  // order of precedence
  // 1. path param
  // 2. query param
  // 3. default value
  name.fname = (nameParts[0] || request.query.fname) ||
    name.fname;
  name.lname = (nameParts[1] || request.query.lname) ||
    name.lname;

  return name;
}

// add the route
server.route({
  method: 'GET',
  path:'/hello/{name*}',
  handler: function (request, reply) {
    // read template and compile using context object
    nunjucks.render('index.html', getName(request), function (err, html) {
      // reply with HTML response
      reply(html);
    });
  }
});

These examples were intentionally contrived for the sake of simplicity so we could focus on the concepts, but in the real world path and query parameters are often used to make service calls or query a database.

Note

In Chapter 8 we will cover routes in more detail, including the creation of an isomorphic router using call, the HTTP router that is used by hapi.

Summary

In this chapter we learned how to serve an HTML document based on a template that rendered dynamic content. We also became more familiar with the request/reply lifecycle and some of its properties, such as request.params and request.query. This knowledge will be used throughout the rest of this part of the book as we build out our application.

Completed Code Examples

You can install the completed code examples from this chapter by executing npm install thaumoctopus-mimicus@"0.2.x" in your terminal.