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!
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:
$npminstallnunjucks–save
Now that we have Nunjucks installed we can create a template, src/index.html, with the contents shown in Example 6-1.
<head><metacharset="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.
importHapifrom'hapi';importnunjucksfrom'nunjucks';// configure nunjucks to read from the dist directorynunjucks.configure('./dist');// create a server with a host and portconstserver=newHapi.Server();server.connection({host:'localhost',port:8000});// add the routeserver.route({method:'GET',path:'/hello',handler:function(request,reply){// read template and compile using context objectnunjucks.render('index.html',{fname:'Rick',lname:'Sanchez'},function(err,html){// reply with HTML responsereply(html);});}});// start the serverserver.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.
// previous code omitted for brevitygulp.task('copy',function(){returngulp.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 brevitygulp.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.
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.
server.route({method:'GET',path:'/hello/{fname}/{lname}',handler:function(request,reply){// read template and compile using context objectnunjucks.render('index.html',{fname:'Rick',lname:'Sanchez'},function(err,html){// reply with HTML responsereply(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.
server.route({method:'GET',path:'/hello/{fname}/{lname}',handler:function(request,reply){// read template and compile using context objectnunjucks.render('index.html',{fname:request.params.fname,laname:request.params.lname},function(err,html){// reply with HTML responsereply(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.
server.route({method:'GET',path:'/hello',handler:function(request,reply){// read template and compile using context objectnunjucks.render('index.html',{fname:request.query.fname,laname:request.query.lname},function(err,html){// reply with HTML responsereply(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.
functiongetName(request){// default valuesletname={fname:'Rick',lname:'Sanchez'};// split path paramsletnameParts=request.params.name?request.params.name.split('/'):[];// order of precedence// 1. path param// 2. query param// 3. default valuename.fname=(nameParts[0]||request.query.fname)||name.fname;name.lname=(nameParts[1]||request.query.lname)||name.lname;returnname;}// add the routeserver.route({method:'GET',path:'/hello/{name*}',handler:function(request,reply){// read template and compile using context objectnunjucks.render('index.html',getName(request),function(err,html){// reply with HTML responsereply(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.
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.