Twilio Functions are the glue that connect user code and Studio to the Twilio environment. They offer a convenient way to execute JavaScript while remaining inside of the Twilio ecosystem. Functions have access to environment variables and shared NPM packages. Functions also have access to the Twilio REST Helper Library. With this Studio flows can be extended to access more APIs. This article provides a few common cases we encountered building Twilio offerings for clients. It also builds upon Understanding Twilio Studio Flow.
Supporting CORS
It may become necessary to make a request from a domain external to the Twilio project. To support Cross-Origin Resource Sharing, CORS, responding to OPTIONS request is necessary. To achieve this consider the following example:
exports.handler = function(context, event, callback) { | |
const response = new Twilio.Response(); | |
response.appendHeader('Access-Control-Allow-Origin', '*'); | |
response.appendHeader('Access-Control-Allow-Methods', 'GET, OPTIONS, PUT, POST, DELETE'); | |
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type'); | |
// check if the event has any data and if not assume this is the OPTIONS request | |
if (Object.keys(event).length === 0) { | |
response.setStatusCode(200); | |
callback(null, response); | |
} else { | |
// call method here to do actual work | |
method(context, event, callback, response); | |
} | |
}; |
The above code relies on the assumption that all OPTIONS requests will have no request body, event is an empty object. This assumption breaks down when dealing with GET requests. A GET request will have no request body. It is possible to support GET, but it would require duplicating the work in both the OPTIONS and GET. I would suggest sticking to CORS-enabled POST request for Twilio Functions that need to be accessed outside of the Twilio environment.
Calling Other Twilio Functions and Assets
Twilio Functions have access to the Runtime Client which provides access to Functions, Assets, and Sync. For now let us ignore Sync and focus on Functions and Assets. A typical Twilio Function will export a handler method. Using the Runtime Client we can get the path of that function and proceed to load that module and call:
exports.handler = function(context, event, callback) { | |
let path = Runtime.getFunctions()['function_name'].path; | |
let fn = require(path); | |
fn.handler(context, event, (error, response) => { | |
if (error) { | |
console.log(error); | |
// handle error case | |
} else { | |
console.log(response); | |
// handle response | |
} | |
}); | |
}; |
Putting common functionality into Assets is another way to share code across Twilio Functions. The code below handles loading an asset JavaScript and calling a method exposed via exports.
function log (data, message) { | |
try { | |
if (data && data.debug) { | |
console.log(message); | |
} | |
} catch (error) { | |
console.log(`Encountered error logging: ${error.message}`); | |
} | |
} | |
module.exports = { | |
log | |
}; |
exports.handler = function(context, event, callback) { | |
let path = Runtime.getAssets()['asset.js'].path; | |
let assetJS = require(path); | |
assetJS.log({ | |
debug: true | |
}, 'asset test message'); | |
callback(null, {}); | |
}; |
A or B
This method has helped in numerous Studio cases where two paths converge and only one value will be present in the Twilio Function call. It handles both JSON and raw data values.
function AorB (event) { | |
try { | |
console.log(JSON.stringify(event)); | |
} catch { | |
// no-op | |
} | |
let response = new Twilio.Response(); | |
response.setStatusCode(200); | |
let body; | |
if (event.a) { | |
body = event.a; | |
} else if (event.b) { | |
body = event.b; | |
} else { | |
console.log('a and b both undefined'); | |
} | |
console.log(`responding with body: ${body}`); | |
if (event.json && event.json.toLowerCase() == 'true') { | |
response.appendHeader('Content-Type', 'application/json'); | |
body = JSON.parse(body); | |
} | |
response.setBody(body); | |
return response; | |
} |