How to Use TypeScript and Deno to Build a CLI

May 20, 2021
Written by
Maciej Treder
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

deno.png

Twilio Programmable SMS includes an HTTP REST API that makes it easy to interact with the SMS API. Once an SMS message request is created with the REST API, its status can be retrieved using another endpoint. By incorporating that REST API you can build a Deno CLI application that sends the message and reports on its delivery.

Deno is a new runtime environment for JavaScript that provides the capabilities of Node.js without the heavyweight package deployment and complex package management required for Node.js applications.

Deno provides new features to support the wide range of contemporary server-side applications being developed with JavaScript, a range that wasn’t envisioned when Node.js was developed.

Deno includes native support for TypeScript; no additional components are required. An application written with Deno can also be launched “remotely” - the user can execute the script specified by the URI pointing to a file hosted on the web. Deno will automatically download the script and other required dependencies to run the program.

Thanks to Deno modularity, you can build a program that utilizes existing code to create an SMS message request and monitor its status. Like other programming languages, Deno supports command line arguments, so you can create a CLI program to achieve anything you want.

Important compliance note: There are rules for using SMS messaging and they vary between countries. Familiarize yourself with the countries’ rules  in which you’ll be sending messages with the Twilio Regulatory Guidelines for SMS.

Understanding the tutorial project

This tutorial will show you how to create a CLI program using Deno. To demo Deno features, you’ll be writing a CLI program that sends an SMS message to a phone number and reports the message delivery status. Unlike the Twilio CLI, this program will report the message delivery status without the need to execute another command. You’ll use the TwilioSMSHelper, which utilizes Twilio Programmable SMS to send the messages with Twilio’s REST API.

You’ll learn how to read command line arguments in Deno using the yargs library. You’ll find out how to register the arguments alias shortcuts (--argument, -a). You’ll also learn how to fallback to environment variables if the user doesn’t provide some of the arguments - for this purpose you will use lodash. You will also learn how to utilize the difference() method from lodash to compare two object keys and verify that the provided input doesn’t miss any values.

Prerequisites

To complete the project described in this tutorial you will need the following tools and resources:

  • Deno – Follow the link for installation instructions for a wide variety of operating systems and package management tools.
  • Twilio account – Sign up for free using this link and receive an additional $10 account credit when you upgrade to a regular account.
  • Twilio Phone Number -  Learn how to register one using Twilio CLI
  • Git – Required for cloning the companion repository or managing the source code as a Git repo.

Visual Studio Code users, ensure that you have the denoland.vscode-deno extension installed:

Screenshot of Deno extension in Visual Studio Code

The Deno extension provides numerous features, including intelligent module import and full intellisense support.

You should also have a working knowledge of the core elements of TypeScript, asynchronous JavaScript mechanics, and ReactiveX programming. Knowledge about Deno essentials is also beneficial; you can learn the basics from the Hello Deno post here on the Twilio blog.

There is a companion repository for this post available on GitHub. It contains the complete source code for the project described in this tutorial and it’s available under an MIT license so you can use it in your own projects.

Getting your Twilio account credentials

To use the Twilio CLI and interact with the Twilio APIs you’ll need three essential pieces of information from your Twilio Console dashboard: Account SID, API Key and API Secret. You can find the Account SID on the top right-hand side of the dashboard. To generate the API Key and API Secret, navigate to Settings/API Keys and click on the red plus button to generate a new key.

Keep these values handy, you’ll need them to test your CLI app in the following steps.

Building the Deno CLI

Initializing the Deno project

Once you’ve registered and tested the phone number, you can initialize the project and its Git repository with a series of command-line instructions.

Open a console window and execute the following instructions in the directory where you want to create the project directory:

mkdir twilio-sms-deno
cd twilio-sms-deno
git init
touch twilioSMSCLI.ts
git add -A
git commit -m "Initial commit"

These commands will create the project directory and the first code file, and initialize a Git repository for the project.

Enabling the Deno extension for Visual Studio Code

If you’re using Visual Studio Code and the Deno extension mentioned in the Prerequisites section, you’ll need to enable the extension for this project.

Create a .vscode/settings.json file in your project folder and add the following JSON:

// .vscode/settings.json
{
 "deno.enable": true,
}

You can also use the VS Code user interface to change the setting for the Workspace. Enabling the Deno extension for the User is not recommended.

Reading command line arguments

To read the arguments provided by the user, you’ll utilize the yargs library. Due to the nature of Deno, you don’t need to install any dependencies - it is enough to add the import statement only.  

Open file twilioSMSCLI.ts and place the following code inside:

import yargs from 'https://cdn.deno.land/yargs/versions/yargs-v16.2.1-deno/raw/deno.ts';

interface Arguments {
  from: string;
  to: string;
  body: string;
  sid: string;
  apikey: string;
  secret: string;
}

let inputArgs: Arguments = yargs(Deno.args)
.alias('f', 'from')
.alias('t', 'to')
.alias('b', 'body')
.alias('i', 'sid')
.alias('k', 'apikey')
.alias('s', 'secret').argv;

console.log(inputArgs);

Apart from importing the yargs library, the above code introduces the Arguments interface that represents a list of arguments accepted by the program. Those arguments are:

  • from - your Twilio number, which will be used to send the message
  • to -  recipient’s phone number
  • body - message content
  • sid - your Twilio Account SID
  • apikey - your Twilio API Key (API SID)
  • secret - secret corresponding to the provided API key

Later on, you’ll parse command line arguments that reside under Deno.args using the yargs library and assign them to the inputArgs variable. Moreover, for each parameter, you define the shortcut, so the user will be able to use the full parameter name or just one letter:

  • --from => -f
  • --to => -t
  • --body => -b
  • --sid => -i
  • --apikey => -k
  • --secret => -s

At the very end, you print received input to the console.

Try out the program using the following commands:

deno run twilioSMSCLI.ts --from +123456788 --to +987654321 --body "Hello Deno" --sid ABCD --apikey EFGH --secret IJKL
deno run twilioSMSCLI.ts -f +123456788 -t +987654321 -b "Hello Deno" -i ABCD -k EFGH -s IJKL
deno run twilioSMSCLI.ts
deno run twilioSMSCLI.ts --to abc  

You should see output like below:

{
  _: [],
  from: "+123456788",
  f: "+123456788",
  to: "+987654321",
  t: "+987654321",
  body: "Hello Deno",
  b: "Hello Deno",
  sid: "ABCD",
  i: "ABCD",
  apikey: "EFGH",
  k: "EFGH",
  secret: "IJKL",
  s: "IJKL",
  $0: "deno run"
}
{
  _: [],
  from: "+123456788",
  f: "+123456788",
  to: "+987654321",
  t: "+987654321",
  body: "Hello Deno",
  b: "Hello Deno",
  sid: "ABCD",
  i: "ABCD",
  apikey: "EFGH",
  k: "EFGH",
  secret: "IJKL",
  s: "IJKL",
  $0: "deno run"
}
{ _: [], $0: "deno run" }
{ _: [], to: "abc", t: "abc", $0: "deno run" }

As you can see, the user can pass the arguments using their full names and shortcuts.

Unfortunately, the program doesn’t verify if the user has provided all of the required input. This may be fixed by enhancing the program with error handling.

First, introduce the lodash library by adding the following import statement at the top of the twilioSMSCLI.ts file:

import * as _ from 'https://deno.land/x/lodash@4.17.15-es/lodash.js';

Add the following code prior toward the bottom of the file, right above the console.log(inputArgs); statement:

let errorMessages: {[k: string]: string} = {
   from: 'Provide the message sender (From:) value using --from [-f] parameter',
   to: 'Provide the message receiver (To:) value using --to [-t] parameter',
   body: 'Provide the message body value using --body [-b] parameter',
   apikey: 'Provide your Twilio API key SID using --apikey [-k] parameter',
   sid: 'Provide your Twilio account SID using --sid [-i] parameter',
   secret: 'Provide your Twilio API key secret using --secret [-s] parameter'
};

let errors: string[] = _.difference(_.keys(errorMessages), _.keys(inputArgs));
if (errors.length > 0) {
   errors.forEach(error => console.log(errorMessages[error]));
   console.log('Proper program usage is: deno run --allow-env --allow-net twilioSMSCLI.ts --from +123456788 --to +987654321 --body "Hello Deno" --sid ABCD --apikey EFGH --secret IJKL');
   Deno.exit(1)
}

You’ve just introduced the errorMessages map object, which holds messages that will be displayed when some of the arguments are missing.

The :{[k:string]: string} type in the object declaration informs the TypeScript compiler that this object will be holding properties of type string under the string keys. Thanks to that, you’ll be able to access these object properties dynamically by using a variable that represents the entry key instead of string literal.

Next, you’ve introduced the errors array that holds information about the missing values in the user input. To feed up this array, you used the difference() method from the lodash library. This method accepts two arrays (A and B) as a parameter and returns a third array, which contains elements from the A array that are not present in the B array (A minus B: A\B). The arrays (A and B) are key-sets of object errorMessages and inputArgs.

In other words, you’re looking for all keys from errorMessages that are not present in the inputArgs.

Finally, you check the errors array length. If it’s more than 0, it means that some input is missing, and an error message should be displayed. Inside the if statement, you iterate through the errors array and print the corresponding error message from the errorMessages. Then, you provide the user with an example program and exit the Deno process by theDeno.exit(1) statement.

Rerun the program to verify if the input is validated against missing values:

deno run twilioSMSCLI.ts

The output will be:

{ _: [], $0: "deno run" }
Provide the message sender (From:) value using --from [-f] parameter
Provide the message receiver (To:) value using --to [-t] parameter
Provide the message body value using --body [-b] parameter
Provide your Twilio API key SID using --apikey [-k] parameter
Provide your Twilio account SID using --sid [-i] parameter
Provide your Twilio API key secret using --secret [-s] parameter
Proper program usage is: deno run --allow-env --allow-net twilioSMSCLI.ts --from +123456788 --to +987654321 --body "Hello Deno" --sid ABCD --apikey EFGH --secret IJKL

At this point, your program now verifies that all arguments have been provided.

Falling back to environment variables when the argument is not provided

Some users might prefer to omit many of the arguments which you are expecting to be provided. Values like Account SID, API key, API secret, and even the Twilio phone number are often kept in environment variables. It would be great not to treat these arguments as necessary ones, and when they are not provided, fallback to the environment variable values.

To introduce the functionality of falling back to environment variables, you’ll use the lodash library.

Now you can utilize the defaults() method from the lodash to fill missing values in inputArgs with the environment variable values, i.e. Deno.env.get('VARIABLE_NAME').

Place the following code before the errorMessages declaration. It’s very important to place this code before the line that looks up the key differences in the errorMessages and inputArgs objects:  let errors: string[] = _.difference(_.keys(errorMessages), _.keys(inputArgs));:

inputArgs = _.defaults(inputArgs, {
   sid: Deno.env.get('TWILIO_ACCOUNT_SID'),
   apikey: Deno.env.get('TWILIO_API_KEY'),
   secret: Deno.env.get('TWILIO_API_SECRET'),
   from: Deno.env.get('TWILIO_PHONE_NUMBER')
});
inputArgs = <any> _.pickBy(inputArgs, _.identity);

At this point, your complete code should look match the following:

import yargs from 'https://cdn.deno.land/yargs/versions/yargs-v16.2.1-deno/raw/deno.ts';
import * as _ from 'https://deno.land/x/lodash@4.17.15-es/lodash.js';

interface Arguments {
   from: string;
   to: string;
   body: string;
   sid: string;
   apikey: string;
   secret: string;
}

let inputArgs: Arguments = yargs(Deno.args)
   .alias('f', 'from')
   .alias('t', 'to')
   .alias('b', 'body')
   .alias('i', 'sid')
   .alias('k', 'apikey')
   .alias('s', 'secret').argv;

let errorMessages: {[k: string]: string} = {
   from: 'Provide the message sender (From:) value using --from [-f] parameter',
   to: 'Provide the message receiver (To:) value using --to [-t] parameter',
   body: 'Provide the message body value using --body [-b] parameter',
   apikey: 'Provide your Twilio API key SID using --apikey [-k] parameter',
   sid: 'Provide your Twilio account SID using --sid [-i] parameter',
   secret: 'Provide your Twilio API key secret using --secret [-s] parameter'
};

inputArgs = _.defaults(inputArgs, {
   sid: Deno.env.get('TWILIO_ACCOUNT_SID'),
   apikey: Deno.env.get('TWILIO_API_KEY'),
   secret: Deno.env.get('TWILIO_API_SECRET'),
   from: Deno.env.get('TWILIO_PHONE_NUMBER')
});
inputArgs = <any> _.pickBy(inputArgs, _.identity);


let errors: string[] = _.difference(_.keys(errorMessages), _.keys(inputArgs));
if (errors.length > 0) {
   errors.forEach(error => console.log(errorMessages[error]));
   console.log('Proper program usage is: deno run --allow-env --allow-net twilioSMSCLI.ts --from +123456788 --to +987654321 --body "Hello Deno" --sid ABCD --apikey EFGH --secret IJKL');
   Deno.exit(1)
}

console.log(inputArgs);

To test that the fallback works, you can save your account credentials and your Twilio phone number in environment variables.

If you are using a Unix-based operating system, such as Linux or macOS, you can set environment variables using the following commands:

export TWILIO_ACCOUNT_SID=<your account sid>
export TWILIO_API_KEY=<your API key>
export TWILIO_API_SECRET=<your API secret>
export TWILIO_PHONE_NUMBER=<your Twilio phone number - including + and country code>

If you are a Windows user, use the following commands:

setx TWILIO_ACCOUNT_SID <your account sid>
setx TWILIO_API_KEY=<your API key>
setx TWILIO_API_SECRET=<your API secret>
setx TWILIO_PHONE_NUMBER <your Twilio phone number>

Run your program and verify that it’s properly falling back to environment variables:

deno run --allow-env twilioSMSCLI.ts --to +987654321 --body "Hello Deno"

You should see output similar to:

{
  _: [ "Deno"" ],
  to: "+987654321",
  t: "+987654321",
  body: ""Hello",
  b: ""Hello",
  $0: "deno run",
  sid: "<your-sid>",
  apikey: "<your-api-key>",
  secret: "<your-secret>",
  from: "<your-twilio-phone-number>"
}

Sending an SMS using Twilio API

It’s time for the program essentials. You’ll take advantage of Deno’s reusability and leverage the code written in the Sending SMS Messages with Deno, TypeScript, and Twilio Messaging post.

Import the TwilioSMS and SMSRequest objects by adding the following statement at the beginning of the twilioSMSCLI.ts file:

import { TwilioSMS, SMSRequest } from 'https://raw.githubusercontent.com/maciejtreder/deno-twilio-messaging/step1/twilioSMS.ts';

At the end of the twilioSMSCLI.ts file, before the console.log(), create the SMSRequest object based on the inputArgs object values; and initialize the TwilioSMS helper method. Next invoke the sendSms() method on the helper object:

const message: SMSRequest = {
   From: inputArgs.from,
   To: inputArgs.to,
   Body: inputArgs.body,
};
const helper = new TwilioSMS(inputArgs.sid, inputArgs.apikey, inputArgs.secret);
helper.sendSms(message).subscribe(console.log);

At the very end, you may want to remove the line containing the console.log() invocation, as it’s no longer needed.

Run your program using an SMS-enabled phone number (replace the <phone_number> with this SMS-enabled phone number):

deno run --allow-env --allow-net twilioSMSCLI.ts --to <phone_number> --body "Hello Deno"

Within short time you should receive an SMS on your device:

screenshot showing new message that says "hello deno" received on phone

Completed code

Your program is ready and should look like the following:

import yargs from 'https://cdn.deno.land/yargs/versions/yargs-v16.2.1-deno/raw/deno.ts';
import * as _ from 'https://deno.land/x/lodash@4.17.15-es/lodash.js';
import { TwilioSMS, SMSRequest } from 'https://raw.githubusercontent.com/maciejtreder/deno-twilio-messaging/step1/twilioSMS.ts';

interface Arguments {
   from: string;
   to: string;
   body: string;
   sid: string;
   apikey: string;
   secret: string;
}

let inputArgs: Arguments = yargs(Deno.args)
   .alias('f', 'from')
   .alias('t', 'to')
   .alias('b', 'body')
   .alias('i', 'sid')
   .alias('k', 'apikey')
   .alias('s', 'secret').argv;

let errorMessages: {[k: string]: string} = {
   from: 'Provide the message sender (From:) value using --from [-f] parameter',
   to: 'Provide the message receiver (To:) value using --to [-t] parameter',
   body: 'Provide the message body value using --body [-b] parameter',
   apikey: 'Provide your Twilio API key SID using --apikey [-k] parameter',
   sid: 'Provide your Twilio account SID using --sid [-i] parameter',
   secret: 'Provide your Twilio API key secret using --secret [-s] parameter'
};

inputArgs = _.defaults(inputArgs, {
   sid: Deno.env.get('TWILIO_ACCOUNT_SID'),
   apikey: Deno.env.get('TWILIO_API_KEY'),
   secret: Deno.env.get('TWILIO_API_SECRET'),
   from: Deno.env.get('TWILIO_PHONE_NUMBER')
});
inputArgs = <any> _.pickBy(inputArgs, _.identity);


let errors: string[] = _.difference(_.keys(errorMessages), _.keys(inputArgs));
if (errors.length > 0) {
   errors.forEach(error => console.log(errorMessages[error]));
   console.log('Proper program usage is: deno run --allow-env --allow-net twilioSMSCLI.ts --from +123456788 --to +987654321 --body "Hello Deno" --sid ABCD --apikey EFGH --secret IJKL');
   Deno.exit(1)
}

const message: SMSRequest = {
   From: inputArgs.from,
   To: inputArgs.to,
   Body: inputArgs.body,
};
const helper = new TwilioSMS(inputArgs.sid, inputArgs.apikey, inputArgs.secret);
helper.sendSms(message).subscribe(console.log);

If you haven’t been following along, but would still like to run the program in this tutorial, you can find the complete code in the companion repository. To clone the repo, execute the following command-line instructions in the directory where you would like to create the project root directory (replace the <phone_number> with SMS-enabled phone number):

git clone https://github.com/maciejtreder/deno-twilio-messaging.git
cd deno-twilio-messaging
git checkout step3
deno run --allow-env --allow-net twilioSMSCLI.ts --to <phone_number> --body "Hello Deno"

Through this article, you’ve learnt how to read command line arguments in Deno using yargs library. You’ve found out how to register the arguments alias shortcuts (--argument -a). You’ve also learnt how to fallback to the environment variable if the user doesn't provide some of the arguments using lodash.

The Deno ecosystem is designed for code reusability. Once the code is published online, you can use it in other Deno programs, or as a standalone one. To try this out, run the following command (replace the <phone_number> with SMS-enabled phone number):

deno run --allow-env --allow-net https://raw.githubusercontent.com/maciejtreder/deno-twilio-messaging/f13de1b1d0392c63e23857127872a86fffc6a87e/twilioSMSCLI.ts -t <phone_number> -b "Hello Deno"

Additional resources

Refer to the following sources for more information discussed in this post:

Hello Deno - Learn the Deno basics.

Asynchronous JavaScript: Introducing ReactiveX and RxJS Observables – Learn to program with the ReactiveX Observables.

Confirming SMS Message Delivery with RxJS Observables, Node.js, and Twilio Programmable SMS - Learn how to send SMS messages using Twilio in Node.js

TwilioQuest – Defeat the forces of legacy systems with this 16-bit style adventure game.

Maciej Treder is a Senior Software Development Engineer at Akamai Technologies. He is also an international conference speaker and the author of @ng-toolkit. You can learn more about him at https://www.maciejtreder.com. You can also contact him at: contact@maciejtreder.com or @maciejtreder on GitHub, Twitter, StackOverflow, and LinkedIn.

Gabriela Rogowska contributed to this post.