Saturday, October 8, 2016

Building a web application using ASP.NET Core 1.0, Node.Js, Express/Hapi/Restify, TypeScript and Visual Studio Code - Part 6, Sec 1

 

Introduction

YouTube Video Link for Part 6, Section 1:

This is the first section of the sixth part of Building a web application using ASP.NET Core 1.0, Node.Js, Aurelia, TypeScript, Webpack in Visual Studio Code.

The source code for this tutorial is available on GitHub.

In this part, I’m going to show how to quickly setup Node.Js, TypeScript, Visual Studio Code and create a REST service using them. We are going to build a ContactManager REST service API in Node.Js using the three popular NodeJs Web frameworks: Express, Hapi and Restify, which we’ll consume from a modern SPA application built using Aurelia, TypeScript and WebPack. I’ll also show you how you can leverage Aurelia’s DI container on the server side inside NodeJs as well as how we can re-use our client side repository interface and InMemoryRepository implementation on the server side.

Installing the Pre-requisites

Before we start installing the pre-requisites, let us create a folder structure to start building our Node.JS REST API Service. Create a folder structure using this command:

mkdir C:\tutorial\part6\modern\nodejs\server\src

and then set the directory as the current directory using this command:

cd C:\tutorial\part6\modern\nodejs\

To install Node.Js, visit nodejs.org and download the Current version. At the time of writing this blog post, the Current version of Node.js is v6.6.0. Node.js is built using Chrome’s V8 JavaScript engine. Microsoft created a project that enables Node.js to optionally use the Microsoft’s ChakraCore JavaScript engine, which is powerful. Checkout the github page of Node.js on ChakraCore here. If you don’t want to spend time compiling Node.js with ChakraCore source code, you can download the releases from here.

Node.js comes with a powerful package manager named NPM (Node Package Manager). We can use npm to install both server and client JavaScript libraries. It is always a good idea to update npm using npm :) It might sound funny but I really mean it because npm has frequent releases than Node.js itself. To update npm, run the command:

npm install npm --g

Before we start installing other node modules, let us create a package.json by running this command:

npm init -y

Since we are going to use TypeScript to develop Node.Js applications, let us install TypeScript 2.0 RTM Release by running the command:

npm install typescript@2.0.3 --g
npm install typescript@2.0.3  --save-dev

Since TypeScript requires the type definition files for JavaScript libraries, let us install the typings tool by running the command:

npm install typings --g

Let us create typings.json using this command:

typings init

Let us create a tsconfig.json file that will contain the settings required by TypeScript compiler. To create tsconfig.json, run the command:

tsc --init

To develop a REST service, we need a good web framework. Express is a fast, unopinionated, minimalist web framework for Node.Js. To install express, please run the command

npm install express body-parser --save

Now, let us install the type definitions required for Express by running the following command:

typings install dt~express dt~serve-static dt~express-serve-static-core dt~mime dt~debug dt~body-parser --save --global

Hapi is another rich framework for building applications and services in Node.js. To install hapi, please run the command:

npm install hapi boom @types/hapi @types/boom --save

Note: Many node modules ship their type definitions in @types namespace and hence you can use npm install @types/<module_name> instead of typings install dt~module_name.

Restify is a very popular NodeJs framework and focus only on providing REST service endpoints unlike Express, which is a full blown web framework that you could use to serve side web pages as well as REST service endpoints. To install restify, please run the command:

npm install restify bunyan --save

Now, let us install the type definitions required for Restify by running the following command:

typings install dt~restify dt~bunyan --save --global

Since we’ll need some string manipulation functions from lodash, let us install lodash using the command:

npm install lodash @types/lodash --save

Creating Project Structure in Visual Studio Code

After we installed the pre-requisites, let us launch VS Code by changing to the project directory in Cmd.exe or Cmder (I prefer the latter) and by running the command:

code .

Make sure to  modify the settings in tsconfig.json to look like this:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "moduleResolution": "node",
        "noImplicitAny": false,
        "sourceMap": true,
        "watch": true    
    },
    "exclude" : [
        "node_modules"
    ]
}

It is time to configure the Task Runner for “TypeScript –tsconfig.json” project type by typing Ctrl + Shift + B and follow the on-screen instructions below:

config-ts-taskrunner

ts-tsconfig.json-taskrunner

After we click on “Configure Task Runner” button and select “TypeScript – tsconfig.json Compiles a TypeScript project” as our Task Runner, VSCode will generate a tasks.json specific to this project type. We don’t need to modify the default values because they are good enough for our project. Once we configure the Task Runner, Ctrl+Shift+B is the key to remember to start/stop i.e., it acts as a toggle command to start the task runner.

Invoke the task runner by clicking Ctrl+Shift+B and you should see that the TypeScript compiler will be happy to compile without any errors and watching the file system for any changes you make to compile the modified files like this:

output-task-runner

Now, let us start creating our application by adding a folder named “src” and a file named index.ts like this:

src-index

As soon as you added the file index.ts, our task runner which is watching for changes, transpiles index.ts into index.js and index.js.map since we chose to generate source maps as well. While it is great to see the generated JavaScript source and source map files to understand that the TypeScript compiler is working, it becomes annoying over a period of time to see 2 extra files in the folder for each .ts file you create. Visual Studio has a nice feature to hide the unwanted files using “File | Preferences | Workspace Settings” menu option. When you select that menu option, we should see a settings.json file under .vscode folder.

file-preferences-workspace-settings 

To make VS Code use TypeScript 2.0.3, add this setting in .vscode\settings.json file:

"typescript.tsdk": "node_modules/typescript/lib"

We need to add the “files.exclude” setting to hide .js and .js.map files:

// Place your settings in this file to overwrite default and user settings.
{
   "typescript.tsdk": "node_modules/typescript/lib",

   // Files

    // Configure glob patterns for excluding files and folders.
   "files.exclude": {
        "**/*.map": {"when": "$(basename)"},
        "**/*.js": {"when": "$(basename).ts"}
    }
}

After you make the changes in settings.json, in order for VS Code’s language service to use TypeScript 2.0.3 installed locally under node_modules/typescript/lib folder, you need to restart VS Code.

If you are interested to learn about all other User and Workspace Settings, please visit the VSCode documentation page at https://code.visualstudio.com/docs/customization/userandworkspace

I don’t like to see the src folder messed up with bunch of .js and .js.map files alongside .ts files. I’d like to keep it clean and also be able to use another editor/IDE like Visual Studio 2015 or 15 to run the project. So, a better approach to organize src and output files is to create a dist folder and configure TypeScript compiler to output the .js and .js.map files into the dist folder using “outDir” setting inside tsconfig.json like this:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "noImplicitAny": false,
	...
	"outDir": "server/dist"
     },
     ...
}

 

We need to make some changes to .vscode/launch.json in order for VS Code debugger to pickup the right source map files so our breakpoints would work just fine. In launch.json , under “configurations” settings, we need to modify “program”, “sourceMaps” and “outDir” settings like this:

"configurations": [
        {
            "name": "Launch",
            "type": "node",
            "request": "launch",
            "program": "${workspaceRoot}/server/src/node-server.ts",
            "stopOnEntry": false,
            "args": [],
            "cwd": "${workspaceRoot}",
            "preLaunchTask": null,
            "runtimeExecutable": null,
            "runtimeArgs": [
                "--nolazy"
            ],
            "env": {
                "NODE_ENV": "development"
            },
            "console": "internalConsole",
            "sourceMaps": true,
            "outDir": "${workspaceRoot}/server/dist"
        }

 

Setting up launch.json to run our Node.Js REST service from VS Code

To run an application from VS Code, simply hit F5 and in the “Select Environment”, choose Node.js. VS Code will create a launch.json under .vscode folder with the configuration required to launch a NodeJs application.

launch-select-environment

Open launch.json under the folder .vscode and change the “program” property to point to “server.ts” and and “sourceMaps” property to true. If you don’t make these changes, you can spend hours or days to figure out why aren’t our breakpoints not working. Our launch.json should look like this:

launch-important-settings

Creating a Contacts REST API using Express, Hapi and Restify frameworks

Since we’ll be creating this application class for all 3 frameworks: express, hapi and restify, let us define a TypeScript interface name INodeJsApplication under the folder named “bootstrap”.

inodejs-application.ts

export interface INodeJsApplication {
    bootstrap(port: number);
}

 

Creating a GET All Contacts API endpoint in Express

 

express-application.ts

"use strict";
import * as express from "express";
import * as debug from "debug";
import * as http from "http";
import {INodeJsApplication} from "./inodejs-application";

export class ExpressApplication implements INodeJsApplication {
  public expressApplication: express.Application;
  private contactRouter: express.Router;
  constructor() {
    // create expressjs application
    this.expressApplication = express();
    // create expressjs router
    this.contactRouter = express.Router();

    // configure application routes
    this.configErrorRoutes();
    this.configApiRoutes();
  }

  private configErrorRoutes() {
    // catch 404 error
    this.expressApplication.use(function(err: any, request: express.Request, response: express.Response, next: express.NextFunction) {
      console.log("Invalid Url");
      response.sendStatus(404);
    });
  }

  private configApiRoutes() {
    this.contactRouter = express.Router();
    this.contactRouter.route("/contacts")
        .get((request: express.Request, response: express.Response) => {
            let id = 0;

            function getId() {
            "use strict";
            return ++id;
            }

            let contacts = [
            {
                id:getId(),
                firstName:"Raja",
                lastName:"Mani",
                email:"rmani@gmail.com",
                phoneNumber:"408-973-5050",
                birthDate: new Date(1973, 5, 1)
            },
            {
                id:getId(),
                firstName:"Jhansi",
                lastName:"Rani",
                email:"jrani@gmail.com",
                phoneNumber:"867-5309",
                birthDate: new Date(1970, 5, 24)
            },
            {
                id:getId(),
                firstName:"Aditi",
                lastName:"Raja",
                email:"araja@gmail.com",
                phoneNumber:"408-973-9006",
                birthDate: new Date(2001, 10, 12)
            },
            {
                id:getId(),
                firstName:"Mahati",
                lastName:"Raja",
                email:"mraja@gmail.com",
                phoneNumber:"408-973-8007",
                birthDate: new Date(2006, 2, 15)
            }
            ];
            response.json(contacts);
        });
    this.expressApplication.use("/api", this.contactRouter);
  }

  public bootstrap(port: number) {
    this.expressApplication.set("port", port);

    // create http server
    let server = http.createServer(this.expressApplication);
    server.listen(port);
    // add error handler
    server.on("error", onError);

    // start listening on port
    server.on("listening", onListening);

    // event listener for HTTP server "error" event.
    function onError(error: any) {
        "use strict";
        if (error.syscall !== "listen") {
            throw error;
        }

        let bind = typeof port === "string"
        ? "Pipe " + port
        : "Port " + port;

        // handle specific listen errors with friendly messages
        switch (error.code) {
        case "EACCES":
            console.error(bind + " requires elevated privileges");
            process.exit(1);
            break;
        case "EADDRINUSE":
            console.error(bind + " is already in use");
            process.exit(1);
            break;
        default:
            throw error;
        }
    }

    /**
     * Event listener for HTTP server "listening" event.
     */
    function onListening() {
        let addr = server.address();
        let bind = typeof addr === "string"
        ? "pipe " + addr
        : "port " + addr.port;
        let debugForExpress = debug("ExpressApplication");
        debugForExpress("Listening on " + bind);
    }
  }
}

Let us create the node-server.ts, which is a very standard code to start a http server influenced by the Google Cloud Platform source code at https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/master/appengine/express/bin/www 

node-server.ts

import {ExpressApplication} from "./contact-manager/bootstrap/express-application";

export class NodeServer {
  constructor() {
    console.log("NodeServer ctor");
  }

  private normalizePort(val: string) : any {
      "use strict";
      let port = parseInt(val, 10);

      if (isNaN(port)) {
        // named pipe
        return val;
      }
      if (port >= 0) {
        // port number
        return port;
      }
      return false;
  }

  bootstrap() {
    // get port from environment and store in Express.
    let port = this.normalizePort(process.env.PORT || 8080);
    let restFramework = process.env.REST_FRAMEWORK || "express";
    let server = new ExpressApplication();

    server.bootstrap(port);
  }
}

let nodeServer = new NodeServer();
nodeServer.bootstrap();

 

Debugging our REST service endpoint in VSCode

Now that we have completed application.ts and server.ts, let us try running it from within VS Code. Let us set a breakpoint on line 75 in application.ts that reads “response.json(contacts);”. We can hit f5 and that should launch the debugger in VS Code using launch.json settings. Then, open up chrome browser and enter http://localhost:8085/api/contacts to see the control comes to the breakpoint inside of VS Code and you can inspect the variables by a simple hover on any variable or switching to the Debug View by pressing Ctrl+Shift+D to inspect variables, call stack etc. like this:

vscode-debug-view-breakpoints

Once we hit Continue i.e., F5, the function will return the JSON value of our contacts array with 4 contact objects. We can see the JSON returned from our API inside of the browser like this:

json-result-from-api-contacts

Organizing our project for cleaner separation

It is not a good idea to create a single Application class and bloat the class with our API implementation methods, routes etc. If you noticed, our get() method creates an array of contacts, which we’ve already implemented using an InMemoryContactsRepository on the client side. Since NodeJs can leverage the client side JavaScript code that we’ve written provided we didn’t do any DOM manipulation or UI specific code, we can use that InMemoryContactsRepository on the server side as well.

Let us organize our project for a cleaner separation. The first and foremost thing is defining our application Model classes that are like our business entities. For a Contacts REST API, it is a no-brainer that we need to define the Contact model. We are going to leverage the power of TypeScript interface to define the Contact Model. We’ll create a folder structure for our Contacts REST API like this:

contact-manager-folder-structure

Then, we’ll copy the client side code under “models” and “services” folders into the server side. Here’s the definition of Under models folder, define IContact like this:

icontact.ts

export interface IContact {
    id: number;
    firstName: string;
    lastName: string;
    email: string;
    phoneNumber: string;
    birthDate: Date;
    thumbnailImage?: string;
}  

The next step in organizing our project is to create a “services” folder and add the interface and classes that will wrap our business logic. For this project, we will define IContactService interface like this:

icontact-service.ts

import {IContact} from "../models/icontact";

export interface IContactService {
    getAll();
    get(id: number);
    search(keyword: string);
    save(contact: IContact);
    create();
    delete(id: number);
}

Before we start defining the concrete implementation classes implementing IContactService, let us see the importance of Promise classes in modern JavaScript applications.

Using Promises in Node.js

In JavaScript, when we call a method that runs asychronously i.e., a method that performs an operation for longer time but returns right away and eventually after it is successfully completed or failed would call the success and error callback functions. We can write a complicated and messy code by adding callbacks, which is commonly called as “callback hell”. In ES6, they have introduced Promise classes that makes our asynchronous code look like synchronous code with flat indentation and one exception channel (i.e., error handling code). Nodejs does not support ES6 promises yet. So we can use TypeScript to bring in ES6 Promise classes or better yet use the powerful Bluebird Promise library for all of our Promise implementations. Since the repository code we brought from the client side already uses Bluebird, we are going to use the Bluebird Promise library on the server side as well. Let us install Bluebird and its type definition using this command:

npm install bluebird @types/bluebird --save 

Let us look at a simple in memory implementation of IContactService that we brought from our client side code:

in-memory-contact-service.ts

import {IContact} from "../models/icontact";
import {IContactService} from "./icontact-service";
var Promise = require("bluebird");
let latency = 200;
let id = 0;

function getId(){
  "use strict";
  return ++id;
}

let contacts = [
  {
    id:getId(),
    firstName:"Raja",
    lastName:"Mani",
    email:"rmani@gmail.com",
    phoneNumber:"408-973-5050",
    birthDate: new Date(1973, 5, 1)
  },
  {
    id:getId(),
    firstName:"Jhansi",
    lastName:"Rani",
    email:"jrani@gmail.com",
    phoneNumber:"867-5309",
    birthDate: new Date(1970, 5, 24)
  },
  {
    id:getId(),
    firstName:"Aditi",
    lastName:"Raja",
    email:"araja@gmail.com",
    phoneNumber:"408-973-9006",
    birthDate: new Date(2001, 10, 12)
  },
  {
    id:getId(),
    firstName:"Mahati",
    lastName:"Raja",
    email:"mraja@gmail.com",
    phoneNumber:"408-973-8007",
    birthDate: new Date(2006, 2, 15)
  }
];

export class InMemoryContactService implements IContactService {
  isRequesting = false;

  constructor() {
    console.log("InMemoryContactService ctor");
  }
  get(id: number) {
    this.isRequesting = true;
    return new Promise((resolve: any, reject: any) => {
      setTimeout(() => {
        let found = contacts.filter( (c: IContact) => c.id === id)[0];
        if(found) {
        resolve(JSON.parse(JSON.stringify(found)));
        } else {
          reject("contact not found");
        }
        this.isRequesting = false;
      }, latency);
    });
  }

  getAll() {
    this.isRequesting = true;
    return new Promise( (resolve: any) => {
      setTimeout(() => {
        let results = contacts;
        resolve(results);
        this.isRequesting = false;
      }, latency);
    });
  }

  search(keyword: string) {
    this.isRequesting = true;
    return new Promise((resolve: any) => {
      setTimeout(() => {
        let results = contacts.filter((c: IContact) => ((c.firstName.indexOf(keyword) !== -1) ||
        (c.lastName.indexOf(keyword) !== -1)));
        resolve(results);
        this.isRequesting = false;
      }, latency);
    });
  }

  save(contact: IContact) {
    this.isRequesting = true;
    return new Promise((resolve: any) => {
      setTimeout(() => {
        let instance = JSON.parse(JSON.stringify(contact));
        let found = contacts.filter((c: IContact) => c.id === contact.id)[0];

        if(found) {
          let index = contacts.indexOf(found);
          contacts[index] = instance;
        } else {
          instance.id = getId();
          contacts.push(instance);
        }

        this.isRequesting = false;
        resolve(instance);
      }, latency);
    });
  }
  create() {
    this.isRequesting = true;
    return new Promise((resolve: any) => {
      setTimeout(() => {
        let newContact = {
          id: getId(),
          firstName:"",
          lastName:"",
          email:"",
          phoneNumber:"",
          birthDate: null
        };
        contacts.push(newContact);
        this.isRequesting = false;
        resolve(newContact);
      }, latency);
    });
  }

  private getContactIndex(id: number) : number {
       return contacts.findIndex((c:IContact) => c.id === id);
    }

  delete(id: number) {
    this.isRequesting = true;
    return new Promise((resolve: any, reject: any) => {
      setTimeout(() => {
        let found = contacts.filter((c: IContact) => c.id === id)[0];
        if(found) {
          console.log("Deleting Contact " + id);
          var index = this.getContactIndex(id);
          if(index !== -1) {
            contacts.splice(index,1);
            console.log("Deleted Contact " + id);
            console.log(contacts);
            resolve(JSON.parse(JSON.stringify(found)));
          }
        } else {
          reject("contact not found");
        }
        this.isRequesting = false;
      }, latency);
    });
  }
}

InMemoryContactService implements the CRUD (Create, Read, Update and Delete) methods besides the search() method. All these methods on the internal array of contacts in memory.

If you notice, we are using findIndex method, which is defined in Array.prototype and is only available in ES6. This is why set the “target” setting to “es6” in our tsconfig.json.  Let us add another useful setting named “allowJs” into our tsconfig.json like this:

{
    "compilerOptions": {
       ,
        "allowJs": true
    },
    "exclude": [
        "node_modules"
    ]
}

Setting “allowJs” to “true” allows us to pass through JavaScript files that are untyped modules. “allowJs” comes in handy when you are migrating your JavaScript code to TypeScript and would like the TypeScript compiler to compile your js files. Remember that you can’t use type annotations on modules imported without type definition files i.e., d.ts.

Refactoring ExpressApplication to use InMemoryContactService

We need body-parser module to parse the body of the http request to handle  POST, PUT and DELETE http verbs. We have already installed body-parser when we installed express and its dependent modules.

We need to import the bodyParser, IContact and InMemoryContactService classes like this:

import * as bodyParser from "body-parser";
import {IContact} from "../models/icontact";
import {InMemoryContactService} from "../services/in-memory-contact-service";

We need to let express use the bodyparser  inside our configApiRoutes() method in application.ts like this:

this.expressApplication.use("/api", bodyParser.json());

Define a contactService property inside the ExpressApplication constructor like this:

export class ExpressApplication implements INodeJsApplication {
  ...
  private contactService: InMemoryContactService;
  constructor() {
    ...
    this.contactService = new InMemoryContactService();
    ...
  }

Change the implementation of get() method to use contactService like this:

private configApiRoutes() {
   this.expressApplication.use("/api", bodyParser.json());
   this.contactRouter = express.Router();
   this.contactRouter.route("/contacts")
       .get((request: express.Request, response: express.Response) => {
           this.contactService.getAll().then((contacts : IContact[]) => {
               response.json(contacts);
           });
       });
   this.expressApplication.use("/api", this.contactRouter);
}

 

Creating a GET All Contacts API endpoint in Hapi

hapi-application.ts
"use strict";
import * as Hapi from "hapi";
import * as Boom from "Boom";
import {INodeJsApplication} from "./inodejs-application";
import {IContact} from "../models/icontact";
import {InMemoryContactService} from "../services/in-memory-contact-service";

export class HapiApplication implements INodeJsApplication {
  public hapiApplication: Hapi.Server;
  private contactService: InMemoryContactService;
  constructor() {
    console.log("HapiApplication ctor");
    // create expressjs application
    this.hapiApplication = new Hapi.Server();
  }

  private configErrorRoutes() {
    // catch 404 and forward to error handler
    this.hapiApplication.ext("onPreResponse", (request: Hapi.Request, reply: Hapi.IReply) => {
       if(request.response.isBoom) {
         if(request.response.output.statusCode === 404) {
           console.log("Invalid Url");
            return reply(Boom.notFound("check your url"));
         }
         return reply(request.response);
       }
       return reply.continue();
    });
  }

  public configApiRoutes() {
      var endpoint = "/api/contacts";
        this.hapiApplication.route({
            path: endpoint,
            method: "GET",
            handler: (request: Hapi.Request, reply: Hapi.IReply) => {
               this.contactService.getAll().then((contacts : IContact[]) => {
                  reply(contacts);
               });
            }});
  }

  public bootstrap(port: number) {
    this.hapiApplication.connection({port: port});
    // configure error routes
    this.configErrorRoutes();
    // configure API routes
    this.configApiRoutes();
    this.hapiApplication.start(() => {
      console.log("Listening on " + this.hapiApplication.info.uri);
    });
  }
}

 

Creating a GET All Contacts API endpoint in Restify

restify-application.ts
"use strict";
import * as restify from "restify";
import {INodeJsApplication} from "./inodejs-application";
import {IContact} from "../models/icontact";
import {InMemoryContactService} from "../services/in-memory-contact-service";

export class RestifyApplication implements INodeJsApplication {
  private restifyApplication: restify.Server;
  private contactService: InMemoryContactService;

  constructor() {
    console.log("RestifyApplication ctor");
    // create restify server
    this.restifyApplication = restify.createServer();
    restify.CORS.ALLOW_HEADERS.push("authorization");
    this.restifyApplication.use(restify.CORS());
    this.restifyApplication.use(restify.pre.sanitizePath());
    this.restifyApplication.use(restify.acceptParser(this.restifyApplication.acceptable));
    this.restifyApplication.use(restify.bodyParser());
    this.restifyApplication.use(restify.queryParser());
    this.restifyApplication.use(restify.authorizationParser());
    this.restifyApplication.use(restify.fullResponse());
    // configure error routes
    this.configErrorRoutes();
    // configure API routes
    this.configApiRoutes();
  }

  private configErrorRoutes() {
    // catch 404 and forward to error handler
    this.restifyApplication.on("NotFound", function(request: restify.Request, response: restify.Response,
     erorr: restify.HttpError, next: restify.Next) {
      console.log("Invalid Url");
      var customError = new Error("Not Found");
      response.send(404, customError);
      return next();
    });
  }

  public configApiRoutes() {
      var endpoint = "/api/contacts";
        this.restifyApplication.get(endpoint, ((request: restify.Request, response: restify.Response, next: restify.Next) => {
           this.contactService.getAll().then((contacts : IContact[]) => {
                response.json(contacts);
                return next();
            });
        }));
  }

  public bootstrap(port: number) {
    let server = this.restifyApplication;
      // listen on provided ports
      server.listen(port, () => {
        console.log("%s listening at %s", server.name, server.url);
      });
  }
}

 

Wiring all 3 frameworks in node-server.ts

Let us import the HapiApplication and RestifyApplication classes like this:

import {HapiApplication} from "./contact-manager/bootstrap/hapi-application";
import {RestifyApplication} from "./contact-manager/bootstrap/restify-application";

It is time for us to use that restFramework variable we defined inside of bootstrap() method. That variable will check if there is an environment variable named “REST_FRAMEWORK” passed as an argument to Node. If a value is passed, it will use that value; othwerise, it will default to “express”. Let us use a switch..case statement to check the value of restFramework and create an instance of one of the frameworks: express, hapi or restify like this:

bootstrap() {
    // get port from environment and store in Express.
    let port = this.normalizePort(process.env.PORT || 8080);
    let restFramework = process.env.REST_FRAMEWORK || "express";
    let server = null;
    switch(restFramework) {
      case "express":
        server = new ExpressApplication();
        break;
      case "hapi":
        server = new HapiApplication();
        break;
      case "restify":
        server = new RestifyApplication();
        break;
    }

    server.bootstrap(port);
  }

 

Creating a Class Factory to abstract the creation of these 3 frameworks

NodeJsFrameworkFactory.ts
import {ExpressApplication} from "./express-application";
import {RestifyApplication} from "./restify-application";
import {HapiApplication} from "./hapi-application";
import * as _ from "lodash";

export class NodeJsFrameworkFactory {
    private frameworkInstances: any[];
    constructor() {
        console.log("NodeJsFramework ctor");
        this.frameworkInstances = new Array(arguments.length);
        this.frameworkInstances["express Application"] = new ExpressApplication();
        this.frameworkInstances["hapi Application"] = new HapiApplication();
        this.frameworkInstances["restify Application"] = new RestifyApplication();
  }
    public createNodeJsFramework(restFramework: string) : any {
        return (restFramework) ? this.frameworkInstances[_.startCase(`${restFramework} Application`)] :
            this.frameworkInstances["expressApplication"];
    }
}

 

Refactoring NodeServer to use NodeJsFrameworkFactory

We will remove the import statements for ExpressApplication, HapiApplication and RestifyApplication and just import NodeJsFrameworkFactory class in node-server.ts like this:

import {NodeJsFrameworkFactory} from "./contact-manager/bootstrap/nodejs-framework-factory";

We’ll replace the switch..case statement inside bootstrap() method like this:

let server = this.nodejsFrameworkFactory.createNodeJsFramework(restFramework);

 

Changing REST_FRAMEWORK environment variable to test all 3 frameworks

We have completed wiring all 3 popular NodeJs frameworks into our NodeServer class. Now, we can add REST_FRAMEWORK environment variable in launch.json to test all 3 frameworks like this:

launch.jsons
"configurations": [
        { 
		... 
		"env": {
                "NODE_ENV": "development",
                "REST_FRAMEWORK": "express"
            	},
		 ... 
		] 
	}

To test Hapi and Restify frameworks, we need to just change the REST_FRAMEWORK to "hapi" or "restify" in launch.json.

Conclusion

In section 1 of part 6 of the series, we have covered a lot of Node.Js server side concepts and used three popular web frameworks: Express, Hapi and Restify to implement an API endpoint using HTTP GET. In section 2 of part 6 of this series, we’ll see how you can refactor the routes out of the application classes and use DI (Dependency Injection) to inject the NodeJsFrameworkFactory, Application, Service and Route classes and use them from NodeServer class. Happy coding!

 

No comments:

Post a Comment