DEV Community

Paul Walker
Paul Walker

Posted on • Originally published at solarwinter.net on

Using Typescript with hapi

I've been using hapi lately, and decided to start using Typescript at the same time. When I looked though there didn't seem to be a lot out there on using them both together. Here's what I learned.

I'm going to assume you have some degree of familiarity with Javascript, along with a basic understanding of what Typescript is.

If that's not the case then I can would definitely recommend reading the MDN JavaScript tutorials, followed by the 5 minute Typescript introduction.

I've chosen to use yarn in the examples below; if you're using npm instead, just change yarn add to npm install.

I've included what you need to get the system up and running, and tried to explain as we go, but this post isn't going to go in depth into any particular point. I've tried to include relevant links as we go, but if I've missed something out then definitely let me know and I'll try to cover it further.

Part 2

Creating the initial server

Admin stuff

First thing - create a project directory and package.json file. Then install hapi for production, since we know we're going to need that.

$ mkdir hapi_typescript
$ cd hapi_typescript
$ yarn init -y
$ yarn add @hapi/hapi
Enter fullscreen mode Exit fullscreen mode

We're also going to need a few more packages in development. Here we're adding the typescript system itself, along with type definitions for hapi and node.

Without installing those, Typescript doesn't know any of the type details for Node or for hapi, so it will complain at something which isn't actually a problem.

We want to be able to see the actual problems, as well as benefit from type-safety using Node and hapi. The type definitions in this post are all coming from DefinitelyTyped.

$ yarn add -D typescript @types/hapi__hapi @types/node
$ yarn add -D nodemon npm-run-all
Enter fullscreen mode Exit fullscreen mode

Next, we have to create a tsconfig.json file with the Typescript options. The easiest thing to do is let tsc do that for us. (tsc is the Typescript compiler executable.)

$ npx tsc --init
Enter fullscreen mode Exit fullscreen mode

For this basic system we're only going to adjust two options - outDir (where to put the compiled Javascript) and rootDir (where the source code lives):

    "outDir": "./lib",
    "rootDir": "./src",
Enter fullscreen mode Exit fullscreen mode

And the final bit of admin - we'll add these scripts to the package.jsonfile, to make it easy to develop.

  • dev:tsc starts the compiler in watch mode, meaning it watches for any changes and automatically rebuilds.
  • dev:serve uses nodemon to automatically reload the server when the Javascript changes.
  • dev uses npm-run-all to run both commands at the same time, so you don't have to have two terminals open.
    "scripts": {
        "dev:tsc": "tsc --watch -p .",
        "dev:serve": "nodemon -e js -w lib lib/main.js",
        "dev": "run-p dev:*"
    }
Enter fullscreen mode Exit fullscreen mode

Lastly, we can change the line in package.json which tells node which file to execute. The scripts don't use it, but it's good practice.

"main": "lib/main.js"
Enter fullscreen mode Exit fullscreen mode

Code!

Right - now we can finally do some coding! We’re going to separate the file containing the server setup code from the file which actually starts it up. It’ll pay off when we start adding tests.

Notice the type declarations for the variables, and the return types on the functions. If you’re going to use Typescript, you might as well benefit from the compiler catching type mistakes.

src/server.ts:

'use strict';

import Hapi from "@hapi/hapi";
import { Server } from "@hapi/hapi";

export let server: Server;

export const init = async function(): Promise<Server> {
    server = Hapi.server({
        port: process.env.PORT || 4000,
        host: '0.0.0.0'
    });

    // Routes will go here

    return server;
};

export const start = async function (): Promise<void> {
    console.log(`Listening on ${server.settings.host}:${server.settings.port}`);
    return server.start();
};

process.on('unhandledRejection', (err) => {
    console.error("unhandledRejection");
    console.error(err);
    process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

src/main.ts:

import { init, start } from "./server";

init().then(() => start());
Enter fullscreen mode Exit fullscreen mode

Giving it a spin

Rather than using yarn dev here, let’s try the stages individually.

$ yarn dev:tsc
[19:54:59] Starting compilation in watch mode...

[19:55:00] Found 0 errors. Watching for file changes.
Enter fullscreen mode Exit fullscreen mode

Well, that looks promising. Ctrl-C out of that and start the server:

$ yarn dev:serve
yarn run v1.22.10
$ nodemon -e js -w lib lib/main.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): lib/**/*
[nodemon] watching extensions: js
[nodemon] starting `node lib/main.js`
Starting server, listening on 0.0.0.0:4000
Enter fullscreen mode Exit fullscreen mode

It’s alive! It’s alive!

Next step

It’s alive, but it doesn't exactly do much. Let's listen for a request and send a reply.

Tests

Since we’re adding functionality, let’s add a test. (Tests are good for the usual reasons; this isn't the place to convince you you need them.)

Setup

We're going to keep our tests in test. We'll use chai and mocha to run them; since we're using Typescript we'll also want to add the relevant type annotations from DefinitelyTyped.

$ mkdir test
$ yarn add -D chai mocha @types/chai @types/mocha
Enter fullscreen mode Exit fullscreen mode

Lastly, we'll use ts-node to directly run these, rather than compiling them to JavaScript.

$ yarn add -D ts-node
Enter fullscreen mode Exit fullscreen mode

We’ll need to tweak tsconfig.json slightly, to tell tsc not to try building the tests. Add this after the end of the compilerOptions object.

"exclude": [
    "test"
]
Enter fullscreen mode Exit fullscreen mode

For the final bit of admin, we’ll add this script to package.json to make it easy to run the tests.

"test": "NODE_ENV=test mocha -r ts-node/register test/**/*.test.ts"
Enter fullscreen mode Exit fullscreen mode

Actual test code!

Finally, time to add our first test!

test/index.test.ts:

import { Server } from "@hapi/hapi";
import { describe, it, beforeEach, afterEach } from "mocha";
import chai, { expect } from "chai";

import { init } from "../src/server";

describe("smoke test", async () => {
    let server: Server;

    beforeEach((done) => {
        init().then(s => { server = s; done(); });
    })
    afterEach((done) => {
        server.stop().then(() => done());
    });

    it("index responds", async () => {
        const res = await server.inject({
            method: "get",
            url: "/"
        });
        expect(res.statusCode).to.equal(200);
        expect(res.result).to.equal("Hello! Nice to have met you.");
    });
})
Enter fullscreen mode Exit fullscreen mode

First thing is to import the various types we’re going to use. Then we import the init function from the server.ts file.

The beforeEach function creates a clean server object before each test, which the afterEach function cleans up. Separating the server code pays off now - we can initialise the server for use in our tests without actually having the overhead of starting it up.

The test itself makes use of the hapi injectmethod, calling into the server code without actually having to go to the effort of making an HTTP call. Here we’re hitting the / route; you can see the test expects the call to succeed, with a nice little message.

Of course, we haven’t written that code yet, so if we run it…

  0 passing (37ms)
  1 failing

  1) smoke test
       index responds:

      AssertionError: expected 404 to equal 200
      + expected - actual

      -404
      +200

      at hapi-typescript/test/index.test.ts:22:35
      [trace continues]
Enter fullscreen mode Exit fullscreen mode

As expected, it fails. Let’s fix that.

Server code

We’re back in server.ts now. First thing is to modify the type imports at the top of the file, to include the Request type:

import { Request, Server } from "@hapi/hapi";
Enter fullscreen mode Exit fullscreen mode

We’ll need that for the index function which does the actual replying for us. In this case it simply returns a string, so we’ve declared that as the return type. Declaring the type of request makes sure we don’t use any properties or methods which aren’t present on that object.

function index(request: Request): string {
    console.log("Processing request", request.info.id);
    return "Hello! Nice to have met you.";
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to connect up the route in the init() function, where the comment in the earlier code indicates.

    server.route({
        method: "GET",
        path: "/",
        handler: index
    });
Enter fullscreen mode Exit fullscreen mode

Now if we run the tests…

  smoke test
    ✓ index responds

  1 passing (33ms)

✨ Done in 2.25s.
Enter fullscreen mode Exit fullscreen mode

Hurrah! Happiness ensues!

Well, that's one route defined in the same file. See the next post to import routes from another file (with appropriate tests).

Latest comments (0)