I love interacting with the Alexa developer community. Your thirst for knowledge constantly challenges me to experiment and dive deep into many voice-related topics. One question I’m often asked at hackathons and training sessions is how to get data from an external API from the AWS Lambda function. It’s a best practice to separate your data from your business logic. Alexa evangelist Amit Jotwani wrote a previous Alexa skill recipe about this topic using version 1 (v1) of the Alexa Skills Kit (ASK) Software Development Kit (SDK) for Node.js. Since then, we’ve updated the SDK to version 2 (v2). V2 offers new challenges and capabilities to think about, including new features that enable you to build skills faster and reduce complexity in your code. This updated skill recipe shows how to make external API calls using v2 of the ASK SDK for Node.js.
To make it simple, we’re going to use the fact skill as our base. Our customers will interact with the skill by asking it for a fact about a number. Let’s take a look at a simple interaction between our skill and our customer:
Customer: Alexa, ask number facts to give me a fact about seven.
Alexa: Seven is the number of main stars in the constellations of the Big Dipper and Orion.
When our customer says, “give me a fact about seven” the GetNumberFactIntent is triggered and the number slot is set to ‘7’. Our skill code makes an http get request to numbersapi.com to get a random fact about the number ‘7’. You can test out the api by typing: http://numbersapi.com/7 into your browser.
When we first built our number facts skill, we hardcoded our facts straight into the code. Let’s take a moment to understand how the code works before we update our code to get the facts from numbersapi.com.
First, we defined an object called numberFacts. It functions as a lookup table. Take a moment to look through the table:
const numberFacts = {
"1":"is the number of moons orbitting the earth.",
"2":"is the number of stars in a binary star system (a stellar system consisting of two stars orbiting around their center of mass).",
"3":"is the number of consecutive successful attempts in a hat trick in sports.",
"4":"is the number of movements in a symphony.",
"5":"is the number of basic tastes (sweet, salty, sour, bitter, and umami).",
"6":"is the number of fundamental flight instruments lumped together on a cockpit display.",
"7":"7 is the number of main stars in the constellations of the Big Dipper and Orion.",
"8":"is the number of bits in a byte.",
"9":"is the number of innings in a regulation, non-tied game of baseball.",
"10":"is the number of hydrogen atoms in butane, a hydrocarbon.",
"11": "is the number of players in a football team."
}
If our customer asked for a fact about ‘7’, we can pass it to numberFacts to get the fact. The code to do that would look like:
numberFacts["7"]
Since we don’t want to hardcode the number 7, let’s take a look at how we’d look up the fact for our number slot.
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
const theFact = numberFacts[theNumber]
Below is the complete code for GetNumberFactIntentHandler before we make any updates.
const GetNumberFactIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'GetNumberFactIntent';
},
handle(handlerInput) {
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
const theFact = numberFacts[theNumber];
const speakOutput = theNumber + theFact;
const repromptOutput = " Would you like another fact?";
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput + repromptOutput)
.getResponse();
}
};
We simply access the number slot and look up the fact using the numberFacts object and then have our skill tell the user the fact by returning a response using the responseBuilder.
Before we make any updates to our code, we need to understand that when we request data from an external API there will be a slight delay until we receive a response. NodeJS technically only has one main code execution thread, so if you do anything that takes a long time no other code can run. This will negatively impact performance. To prevent this from happening, most things that can potentially take a long time to complete are written to be asynchronous. This will allow the code defined after it to run. To demonstrate how this works when the user says “give me a fact about 7” take a look at the following code:
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
// making an http get request is slow, so getHTTP is asynchronous
const theFact = getHttp("http://numbersapi.com/" + theNumber);
console.log("The fact is: ", theFact);
const speakOutput = theNumber + theFact;
return handlerInput.responseBuilder
.speak(speakOutput)
.getResponse();
The getHttp function is asynchronous thus it won’t pause execution and it won’t be complete before the console.log executes on the line below. If you check your logs, you will see “The fact is: undefined”, which is not good. Further making things worse, Alexa will say, “7 undefined.” This will confuse our customer, so let’s keep working at this.
How do we wait for the response without blocking the main executition thread? We don’t. We pass getHttp a block of code to execute when it finishes! We’re basically saying, “hey getHttp, once you finish getting that stuff for us, follow these instructions to process it.” Let’s take a look at how that works:
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
// making an http get request is slow, so getHTTP is asynchronous
const theFact = getHttp("http://numbersapi.com/" + theNumber, response => {
console.log("The fact is: ", theFact);
const speakOutput = theNumber + response;
return handlerInput.responseBuilder
.speak(speakOutput)
.getResponse();
});
Here we are passing an arrow function to getHttp, which is the code we want to have execute when we get a response back from numbersapi.com.
Now that we understand how asynchronous functions work. Let’s take a look how we’ll define the getHttp function. We’ll be using the built-in http module to make the request. So, we’ll need to require the http module in order to use it:
const http = require('http');
The get function is asynchronous, so getHttp will need to be asynchronous as well. To do this, we are going to use promises. Like we did above, we could use an arrow function, but using a promise now will enable us to cut a lot of complexity out of our code when we get to recipe 2.
A promise is an object that represents the success or failure of an asynchronous task. When you define a promise, you define a closure that the promise executes. In that code you determine success or failure by calling either resolve or reject, respectively. When you create the Promise, the code doesn’t run until you call the then method. The then method takes a block of code (like an arrow function which we used above) which will run upon success. Any variables you pass to resolve will be available in then. To handle any failures, you can provide a block of code for catch. Likewise, any variables you passed to reject will be available within catch.
Let’s take a look at how you’d create a promise. We’re going to have getHttp return a promise that wraps the http.get function, but now let’s take a high level look at how it would create the function.
function getHttp(url, query) {
return new Promise((resolve, reject) => {
// set up the http request
...
if (error) {
reject(error);
} else {
resolve(response)
}
});
}
The function creates and returns a new Promise. We will call reject and if it completes without any errors we call resolve and pass it the response so the arrow function we pass to then can access it.
getHttp returns a promise which is an object. The following code would create the promise but won’t execute the asynchronous code.
const URL = "http://numbersapi.com/";
const theNumber = "7";
getHttp(URL, theNumber) // returns a promise, but doesn't execute the request
To make the promise execute, we would call then and pass it an arrow function:
getHttp(URL, theNumber)
.then(response => {
// the code here will run when the request completes.
})
.catch(error => {
// the code here will run if there's an error.
});
Now let’s take a look at our getHttp function. It takes in 2 parameters, url and query. The url is the address of the api we want to get and query is the value we want to look up. It uses the http.get function. It takes a URL and an arrow function. We combine the url and query together to make URL and pass it to http.get. In the arrow function we subscribe to events called data, end, and error.
When you make a request you might not get all the data in one piece, so you’ll need to keep appending the chunks of data until the request ends. Take a look at the block below to see how we are appending the chunks of data:
...
let returnData = '';
response.on('data', chunk => {
returnData += chunk;
});
...
If there’s an error we’ll receive an error event. This is where we’ll want to call reject.
response.on('error', error => {
reject(error);
});
There’s another place to check for errors. If we get a status code from the server that indicates an error, we’ll also want to call reject.
if (response.statusCode < 200 || response.statusCode >= 300) {
return reject(new Error(`${response.statusCode}: ${response.req.getHeader('host')} ${response.req.path}`));
}
If the request completes without an error, then we’ll receive an end event. In that case, we’ll want to call resolve.
response.on('end', () => {
resolve(returnData);
});
Zooming out the entire function appears as below:
const getHttp = function(url, query) {
return new Promise((resolve, reject) => {
const request = http.get(`${url}/${query}`, response => {
response.setEncoding('utf8');
let returnData = '';
if (response.statusCode < 200 || response.statusCode >= 300) {
return reject(new Error(`${response.statusCode}: ${response.req.getHeader('host')} ${response.req.path}`));
}
response.on('data', chunk => {
returnData += chunk;
});
response.on('end', () => {
resolve(returnData);
});
response.on('error', error => {
reject(error);
});
});
request.end();
});
}
Now that we’ve made our getHttp function return a promise, we’ll use it from our handle function to make the http request:
getHttp(URL, theNumber).then(response => {
speakOutput += " " + response;
return handlerInput.responseBuilder
.speak(speakOutput + repromptOutput)
.reprompt(repromptOutput)
.getResponse();
}).catch(error => {
console.log('Error with HTTP Request:', error);
repromptOutput = "";
return handlerInput.responseBuilder
.speak(`I wasn't able to find a fact for ${theNumber}. ${repromptOutput}`)
.reprompt(repromptOutput)
.getResponse();
});
Our then method takes an arrow function that handles the response that comes back from numbersapi.com and adds it to the speakOutput. It then builds the response using the responseBuilder and returns it. The catch method takes arrow function that logs the error and communicates that there was an error to the customer. Putting it all together, our handle function looks like:
handle(handlerInput) {
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
const speakOutput = theNumber ;
const repromptOutput = " Would you like another fact?";
getHttp(URL, theNumber).then(response => {
speakOutput += " " + response;
return handlerInput.responseBuilder
.speak(speakOutput + repromptOutput)
.reprompt(repromptOutput)
.getResponse();
}).catch(error => {
return handlerInput.responseBuilder
.speak(`I wasn't able to find a fact for ${theNumber}`)
.reprompt(repromptOutput)
.getResponse();
});
}
If you were to use this code, however, your skill will say, “There was a problem with the requested skill’s response.” What happened?
Once you go asynchronous all sub functions must also be ansynchronous. Our getHttp function returns a Promise object. We call then and the code executes, but execution hasn’t been paused. The handle function completes. So when we return the response using the responseBuilder in our arrow function, the response, which is what your skill will say, isn’t being returned from your handle function. To demonstrate, add the following code below the catch and your skill will say: “We spoke before the request completed.”
return handlerInput.responseBuilder
.speak("We spoke before the request completed")
.getResponse();
You code should now look like:
handle(handlerInput) {
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
const speakOutput = theNumber ;
const repromptOutput = " Would you like another fact?";
getHttp(URL, theNumber).then(response => {
speakOutput += " " + response;
return handlerInput.responseBuilder
.speak(speakOutput + repromptOutput)
.reprompt(repromptOutput)
.getResponse();
}).catch(error => {
return handlerInput.responseBuilder
.speak(`I wasn't able to find a fact for ${theNumber}`)
.reprompt(repromptOutput)
.getResponse();
});
return handlerInput.responseBuilder
.speak("We spoke before the request completed")
.getResponse();
}
So how do we make sure our handle function returns response from our promise? We return a promise! We can wrap the getHttp function in a new promise and call resolve and reject instead of return.
return new Promise((resolve, reject) => {
getHttp(URL, theNumber).then(response => {
speakOutput += " " + response;
resolve(handlerInput.responseBuilder
.speak(speakOutput + repromptOutput)
.reprompt(repromptOutput)
.getResponse());
}).catch(error => {
reject(handlerInput.responseBuilder
.speak(`I wasn't able to find a fact for ${theNumber}`)
.getResponse());
});
});
Once we have returned our promise, the SDK will call then on our promise and use the response it receives. This response is the one we built using the responseBuilder. It includes the data we requested from numbersapi.com. Let’s take a look at the whole handle function:
handle(handlerInput) {
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
const speakOutput = theNumber ;
const repromptOutput = " Would you like another fact?";
return new Promise((resolve, reject) => {
getHttp(URL, theNumber).then(response => {
speakOutput += " " + response;
resolve(handlerInput.responseBuilder
.speak(speakOutput + repromptOutput)
.reprompt(repromptOutput)
.getResponse());
}).catch(error => {
reject(handlerInput.responseBuilder
.speak(`I wasn't able to find a fact for ${theNumber}`)
.getResponse());
});
});
}
const getHttp = function(url, query) {
return new Promise((resolve, reject) => {
const request = http.get(`${url}/${query}`, response => {
response.setEncoding('utf8');
let returnData = '';
if (response.statusCode < 200 || response.statusCode >= 300) {
return reject(new Error(`${response.statusCode}: ${response.req.getHeader('host')} ${response.req.path}`));
}
response.on('data', chunk => {
returnData += chunk;
});
response.on('end', () => {
resolve(returnData);
});
response.on('error', error => {
reject(error);
});
});
request.end();
});
}
Promises are exciting, and I’ve only scratched the surface on how they can be used. Our code has become a little hard to read because we’ve nested our getHttp function that returns a promise in a new promise. Let’s use some relatively new Javascript keywords to simplify our code async and await.
The async keyword is used to denote that following function returns a promise. The cool thing is that even if the function returns a non-promise value, Javascript will wrap it in a resolved promise so you don’t have to.
We can use this wonderful keyword to declare that our handle function returns a promise and instead of nesting our call to getHttp in a new promise, we’ll let Javascript do the hard work.
async handle(handlerInput) {
...
}
Now let’s talk about await. This keyword makes function execution pause until the promise it precedes has completed. This doesn’t mean that we’re pausing execution of the main thread. We’re simply pausing execution of our async function. In our case, we’re going to pause handle until getHttp has been resolved or rejected. Let’s take a look at how we’d use the await keyword.
const response = await getHttp(URL, theNumber);
Isn’t that much cleaner! We don’t have to call then nor do we have to pass in an arrow function. Javascript pauses the execution in this case. The response will be the value that we passed to the resolve function when we defined our promise in getHttp.
So what happens if there’s an error? With our promise we called catch to handle the error case. When we use the await keyword the promise returns the resolved value, so if there’s an error it throws it. So we need to wrap our code in a try/catch block.
try {
const response = await getHttp(URL, theNumber);
}
catch (error) {
// handle the error
}
If there’s an error, the code in our catch block will execute so we can log the error and ask our customer for another number to look up. We can build our response with the response builder inside the try and catch which will allow us to have only one return statement at the end of our handle function which will make our code easier to read. Our complete function that uses async and await appears below.
async handle(handlerInput) {
const theNumber = handlerInput.requestEnvelope.request.intent.slots.number.value;
let speakOutput = theNumber ;
const repromptOutput = " Would you like another fact?";
try {
const response = await getHttp(URL, theNumber);
speakOutput += " " + response;
handlerInput.responseBuilder
.speak(speakOutput + repromptOutput)
.reprompt(repromptOutput)
} catch(error) {
handlerInput.responseBuilder
.speak(`I wasn't able to find a fact for ${theNumber}`)
.reprompt(repromptOutput)
}
return handlerInput.responseBuilder
.getResponse();
}
const getHttp = function(url, query) {
return new Promise((resolve, reject) => {
const request = http.get(`${url}/${query}`, response => {
response.setEncoding('utf8');
let returnData = '';
if (response.statusCode < 200 || response.statusCode >= 300) {
return reject(new Error(`${response.statusCode}: ${response.req.getHeader('host')} ${response.req.path}`));
}
response.on('data', chunk => {
returnData += chunk;
});
response.on('end', () => {
resolve(returnData);
});
response.on('error', error => {
reject(error);
});
});
request.end();
});
}
Our handle function is so clean and we didn’t even have to make any updates to the getHttp function to take advantage of the async/await because we made it return a promise from the beginning. The await keyword works only with promises. If you’re using an asychronous function that doesn’t return a promise, you can wrap it a promise so you can use await. This is why we made our getHttp function return a promise.
Go ahead and try out this new skill recipe. You can replace the numbersapi with your own external api. The internet is your oyster. Putting this technique to use will open up your skill to access data from outside of the values you’ve defined in code.
I hope this post has inspired you to think about ways that you can augement that data that your skill uses by using external APIs. I would love to hear your ideas. Please share them with me on Twitter at @SleepyDeveloper.
For more recipes, visit the Alexa Skill-Building Cookbook on GitHub.