Share Code Between Twilio Functions

Here's how to share code in Twilio Functions.

Normal imports

Sharing code in a Twilio Function isn't as straightforward as one might hope.

Normally in Nodejs, I could:

import { getMessage } from './lib/message.js'

But no...

Serverless

Serverless tech like AWS Lambda or Twilio Functions (probably built on AWS Lambda) keeps things more siloed. There's usually just a file with a function that handles requests. Getting shared code into that function has been more of a chore.

Assets and Functions

In Twilio land, there are scripts that you can publish under the functions/ folder or the assets/ folder.

Functions are exposed to be addressed with http requests based on the directory and file structure. When addressed thusly, they are invoked, do work and return a response.

Assets can be scripts, images, stylesheets, etc. These are static assets.

When you want one of your Twilio Functions to reuse some other bit of code in your Twilio Functions environment, you could put that code in the functions or assets directory. Most of the time, with shared code, I think it makes more sense to think of it as a shared asset.

Private Assets

To make an asset available to your Twilio Function but not a client via http, you can mark it as private. You do that with a filename suffix of .private.js.

For example, I create a file called message.private.js in my project subdirectory of 'assets/lib/':

assets/lib/message.private.js

Export Public API

In Node, to expose a variable in a module to the outside, it must be exported. In Twilio Functions as of this writing, the latest supportable version is nodejs@16. This means that CommonJS will be required, and ES Module syntax is unavailable.

So our export might look like:

exports.getMessage = function myFunction() {
  return 'Super rad'
}

Available to Import

To start Twilio Functions environment locally, you'll need the twilio-cli, and you'll run:

twilio serverless:dev

When it starts successfully, your available functions and assets will be listed, including this new one:

│   Twilio assets available:                                                                         │
│   ├── [private] /lib/message.js | Runtime.getAssets()['/lib/message.js']                                   │

Make sure to restart the process after adding a new asset file instead of relying on the hotreloading. The process will say your new code is available, but the getAssets() (discussed in a moment) call won't have that entry.

Import Shared Code

Now in your Twilio Function, you can import the asset and use the getMessage function.

You have a handler method in your Function, and you import using the odd syntax here:

module.exports = function handleRequest(context, event, callback) {
  const { getMessage } = require(Runtime.getAssets()['/lib/message.js'].path)

  callback(null, { message: getMessage() })
}

To explain: Where the actual path to /lib/message.js is, is unknown to you. The Twilio Functions environment has put it somewhere. Now we use its original file system path as the key to get that special location. Once we get the .path, we plug that into a dynamic require() to import the module. Then we're destructuring { getMessage } out of the named exports.

Voila. Piece of chocolate cake, no.

Alternative: Import Function

If you had chosen to put your shared code into functions/, it would show up in a different collection. Instead of Runtime.getAssets(), you could Runtime.getFunctions() in the same way.

But, again, I think it makes more sense to think of a common library as an asset than a function. You shouldn't be able to curl your library.