An Alexa Node.js TypeScript Boilerplate Project

I am currently looking at the Alexa Gadget Skills API for the purpose of creating a fun game for my son and presenting on the experience at a few conferences.  Even though the SDK is written in TypeScript, the Trivia Game sample is not. That irked me a bit. It was particularly painful as the sample does some odd things using globals that was difficult to track. So, before I create a series around how I created a few Echo Button enabled games on Alexa, I present a simple, bare bones Alexa Skill TypeScript Boilerplate Project. You can find the code on GitHub.

The V2 version of the SDK has some really good improvements over the first version. The documentation is quite good. The core concepts that we should keep in mind for our boilerplate are:

  • The index file declares the skill and pull in all necessary handlers to compose a lambda handler.
  • Each RequestHandler has two methods: canHandle and handle. Handlers are called in the order they were registered. The first handler to evaluate canHandle to true is selected for processing and itshandle method is invoked.
  • The index file registers instances of RequestInterceptor and ResponseInterceptor. A RequestInterceptor is code that executes and can perform actions on an incoming message before a handler is selected. Likewise, a ResponseInterceptor is code that executes after a handler has finished executing. The most obvious use case for these two is message logging, though, as per the sample above, developers can get much more creative.
  • Lastly, the index declares a custom ErrorHandler. In the previous version of the SDK, when an error occurs, the Alexa device responds with the dreaded “There was a problem with the skill’s response”. Now, we log the error and respond with a friendly message to the user.

We start by taking advantage of the Alexa Skills Kit CLI. If you are doing Alexa development without using it, what is wrong with you? Go ahead and get setup with it before continuing. If we go ahead and create a new skill by using ask new -n TestSkill, we get a basic Skill Manifest (skill.json), the interaction model in the default culture (models/en-US.json) and code for the skill under lambda/custom.

The generated skill interaction model includes Amazon’s builtin intents: AMAZON.CancelIntent, AMAZON.HelpIntent and AMAZON.StopIntent. The default interaction model also includes a custom HelloWorldIntent. The auto-generated index.js file includes all the code to handle all intents, plus a LaunchRequest handler and a custom ErrorHandler. The full code is shown below.


/* eslint-disable  func-names */
/* eslint-disable  no-console */

const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  handle(handlerInput) {
    const speechText = 'Welcome to the Alexa Skills Kit, you can say hello!';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withSimpleCard('Hello World', speechText)
      .getResponse();
  },
};

const HelloWorldIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent';
  },
  handle(handlerInput) {
    const speechText = 'Hello World!';

    return handlerInput.responseBuilder
      .speak(speechText)
      .withSimpleCard('Hello World', speechText)
      .getResponse();
  },
};

const HelpIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
  },
  handle(handlerInput) {
    const speechText = 'You can say hello to me!';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withSimpleCard('Hello World', speechText)
      .getResponse();
  },
};

const CancelAndStopIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
        || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
  },
  handle(handlerInput) {
    const speechText = 'Goodbye!';

    return handlerInput.responseBuilder
      .speak(speechText)
      .withSimpleCard('Hello World', speechText)
      .getResponse();
  },
};

const SessionEndedRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
  },
  handle(handlerInput) {
    console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`);

    return handlerInput.responseBuilder.getResponse();
  },
};

const ErrorHandler = {
  canHandle() {
    return true;
  },
  handle(handlerInput, error) {
    console.log(`Error handled: ${error.message}`);

    return handlerInput.responseBuilder
      .speak('Sorry, I can\'t understand the command. Please say again.')
      .reprompt('Sorry, I can\'t understand the command. Please say again.')
      .getResponse();
  },
};

const skillBuilder = Alexa.SkillBuilders.custom();

exports.handler = skillBuilder
  .addRequestHandlers(
    LaunchRequestHandler,
    HelloWorldIntentHandler,
    HelpIntentHandler,
    CancelAndStopIntentHandler,
    SessionEndedRequestHandler
  )
  .addErrorHandlers(ErrorHandler)
  .lambda();

The code is pretty straightforward. Each handler’s canHandle method checks for the right request type and intent name to be present. The ErrorHandler logs the error and asks the user to repeat themselves. Lastly, the code composes the skill and returns an AWS Lambda handler.

We now turn this code into TypeScript. On top of that, we break the file up into different handler files and add intercept handlers. I work using Visual Studio Code, so I also enable tslint, a TypeScript linter so I can get extra tips of fixing up my TypeScript.

We first need to create the tsconfig.json file. This file basically provides options for the TypeScript compiler. More details can be found here. We will go ahead and write all of our TypeScript code in a folder called src. The destination will be root directory. That way, once tsc compiles everything, we run ask deploy to push the skill code into Lambda.

We use the following tsconfig.json file as a starting point.


{
    "include": [
        "src/**/*"
    ],
    "exclude": [

    ],
    "compilerOptions": {
        "lib": [
            "dom",
            "es2017"
        ],
        /* Basic Options */
        "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
        "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */
        "sourceMap": true, /* Generates corresponding '.map' file. */
        "outDir": ".", /* Redirect output structure to the directory. */
        "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
        "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
        "strict": true,
        "noUnusedLocals": true
    }
}

We will place this file inside the lambda/custom directory. Next, we add two scripts into ourpackage.json file.


  "scripts": {
    "build": "tsc",
    "watch": "tsc --watch"
  }

You should have the Node.js TypeScript package installed for this to work.
npm install -g typescript

Let’s confirm the setup works. Create the src directory and place the following content into an index.ts file.


console.log("Hello World!");

If we run npm run build in the custom/lambda directory, the index.js gets created in the root. Good job! Our directory layout should look as follows. (You can also run npm run watch, which makes sure that any time a TypeScript file is modified, the compiler runs)

Next, we create theinterceptors and handlers directory. Inside of those, we create the files to support all the different handlers and interceptors.

You can see where this is going. We now fill out the code for each of these files. Once done and compiled into JavaScript, we run ask deploy and our skill will just work.

We begin with the Launch handler in the Launch.ts file.


import { HandlerInput, RequestHandler } from "ask-sdk-core";
import { Response } from "ask-sdk-model";

export class LaunchHandler implements RequestHandler {
    canHandle(handlerInput: HandlerInput): boolean {
        const request = handlerInput.requestEnvelope.request;
        return request.type === "LaunchRequest";
    }

    handle(handlerInput: HandlerInput): Response {
        const speechText = "Welcome to the Alexa Skills Kit, you can say hello!";

        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .withSimpleCard("Hello World", speechText)
            .getResponse();
    }
}

Note that the ask-sdk-core and ask-sdk-model modules both include TypeScript declaration files making development in an environment like Visual Studio Code easier.

Every other handler looks similar. For example, the HelloWorldHandler looks as follows:


import { HandlerInput, RequestHandler } from "ask-sdk-core";
import { Response } from "ask-sdk-model";

export class HelloWorldHandler implements RequestHandler {
    canHandle(handlerInput: HandlerInput): boolean {
        const request = handlerInput.requestEnvelope.request;
        return request.type === "IntentRequest" && request.intent.name === "HelloWorldIntent";
    }

    handle(handlerInput: HandlerInput): Response {
        const speechText = "Hello World!";

        return handlerInput.responseBuilder
            .speak(speechText)
            .withSimpleCard("Hello World", speechText)
            .getResponse();
    }
}

The CustomErrorHandler logs the error and provides a friendly response.


import { HandlerInput, ErrorHandler } from "ask-sdk-core";
import { Response } from "ask-sdk-model";

export class CustomErrorHandler implements ErrorHandler {
    canHandle(handlerInput: HandlerInput): boolean {
        return true;
    }

    handle(handlerInput: HandlerInput, error: Error): Response {
        const request = handlerInput.requestEnvelope.request;

        console.log(`Error handled: ${error.message}`);
        console.log(`Original Request was: ${JSON.stringify(request, null, 2)}`);

        return handlerInput.responseBuilder
            .speak("Sorry, I can not understand the command.  Please say again.")
            .reprompt("Sorry, I can not understand the command.  Please say again.")
            .getResponse();
    }
}

The intercept handlers are similar, here is what the request one looks like.


import { RequestInterceptor, HandlerInput } from "ask-sdk-core";

export class RequestLoggingInterceptor implements RequestInterceptor {
    process(handlerInput: HandlerInput): Promise {
        return new Promise((resolve, reject) => {
            console.log("Incoming request:\n" + JSON.stringify(handlerInput.requestEnvelope.request));
            resolve();
        });
    }
}

The last code we will show in this post is the index.ts file that puts it all together. It imports all of the different modules we just created and ensures they are exposed to the Azure handler.


import { SkillBuilders } from "ask-sdk-core";

import { BuiltinAmazonCancelHandler } from "./handlers/builtin/AMAZON.CANCEL";
import { BuiltinAmazonHelpHandler } from "./handlers/builtin/AMAZON.Help";
import { BuiltinAmazonStopHandler } from "./handlers/builtin/AMAZON.Stop";
import { LaunchHandler } from "./handlers/Launch";
import { HelloWorldHandler } from "./handlers/HelloWorld";
import { SessionEndedHandler } from "./handlers/SessionEndedRequst";

import { CustomErrorHandler } from "./handlers/Error";
import { RequestLoggingInterceptor } from "./interceptors/RequestLogging";
import { ResponseLoggingInterceptor } from "./interceptors/ResponseLogging";

function buildLambdaSkill(): any {
    return SkillBuilders.custom()
        .addRequestHandlers(
            new LaunchHandler(),
            new HelloWorldHandler(),
            new BuiltinAmazonCancelHandler(),
            new BuiltinAmazonHelpHandler(),
            new BuiltinAmazonStopHandler(),
            new SessionEndedHandler()
        )
        .addRequestInterceptors(new RequestLoggingInterceptor())
        .addResponseInterceptors(new ResponseLoggingInterceptor())
        .addErrorHandlers(new CustomErrorHandler())
        .lambda();
}

export let handler = buildLambdaSkill();

Once all code is in place, we make sure that TypeScript compiled everything and run ask deploy. If all goes well, our skill and lambda function will be updated to reflect the changes. Below, I show the Test tab’s output as I type through a conversation. It all works great! Notice, I left the default invocation name as “greeter”. We’ll change that later on.

That’s it! For now, I am not placing any effort in terms of supporting automated testing or providing a strong opinion as to where the business logic lives. Some of this may change as I develop more skill. In the mean time, you can find all the code in this GitHub repo.