Build a Video API with Twilio Programmable Video, Node.js, TypeScript, and Express

March 19, 2021
Written by
Mia Adjei
Twilion
Reviewed by
Diane Phan
Twilion

Build a Video API with Twilio Programmable Video, Node.js, TypeScript, and Express

This article is for reference only. We're not onboarding new customers to Programmable Video. Existing customers can continue to use the product until December 5, 2024.


We recommend migrating your application to the API provided by our preferred video partner, Zoom. We've prepared this migration guide to assist you in minimizing any service disruption.

Have you ever wanted to include video in your app but weren’t sure how to get started? Well, Twilio Programmable Video provides a friendly API you can use to make this happen.

This tutorial will show you how to build an API to create and manage video rooms programmatically using Twilio Programmable Video, Node.js, TypeScript, and Express.

Prerequisites

  • A free Twilio account. (If you register here, you'll receive $10 in Twilio credit when you upgrade to a paid account!)
  • Node.js installed on your machine.
  • HTTPie or cURL.

Set up your environment

Open your terminal, find the place you want to set up your project, and create a new directory called express-video-api where your project will live:

mkdir express-video-api
cd express-video-api

Next, set up a new Node.js project with a default package.json file by running the following command:

npm init --yes

Install dependencies

Express is the backend framework for Node.js that you'll be using in this tutorial to build out your API. Add Express to your project by running the following command:

npm install express

Then, add TypeScript and the required types for Node.js and Express to your project. Since you only need these during development, you can add them as devDependencies by running the following command:

npm install --save-dev typescript @types/express @types/node

Add ts-node and node-dev to the project as well, using the command below. These packages will allow your Node.js process to reload whenever you change something in your files. Adding dotenv also allows you to load environment variables into your project from a .env file.

npm install --save-dev node-dev ts-node dotenv

Finally, install the twilio package, using the command below. This package will let you access the Twilio Programmable Video APIs.

npm install twilio

If you check your package.json file now, you should notice that all of the packages above have been installed as either dependencies or devDependencies.

Secure and configure environment variables

Generate an API key

The next step is to generate an API key. Log in to the Twilio console, and from the Dashboard, click on Settings in the left menu. Then click on the API Keys submenu item.

Twilio console, showing navigation to API Key generation page: Dashboard > Settings > API Keys

Click on the red plus (+) icon (shown above) if you have other API keys already; otherwise click the Create new API Key button.

When prompted, give your API key a friendly name like "Video API Tutorial Key", so you'll remember what this key is for in the future. Then, select your key type. For this tutorial, a Standard key will work fine. Take note of the API key and secret that are generated, and keep them in a secure place.

Store environment variables in a .env file

Next, create a .env file at the root of your project. This is where you will keep your Twilio account credentials.

touch .env

Open the .env file in your text editor and add the following variables:

TWILIO_ACCOUNT_SID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

You’ll need to replace the placeholder text above with your actual Twilio credentials.

Replace the placeholders for TWILIO_API_KEY and TWILIO_API_SECRET with the API key and secret you generated in the previous step.

Then, return to the Twilio Console to find your Account SID.

Twilio console, showing location of Account SID and Auth Token

Copy and paste the value for Account SID into your .env file.

Now you have all of the credentials you will need for this project. It’s very important to keep these credentials safe, so you’ll want to create a .gitignore file to avoid committing the .env file to your git repository.

In the root directory of your project, create the .gitignore file using the following command:

touch .gitignore

Then open .gitignore in your code editor and add the .env file:

.env

Access environment variables from a config.ts file

Now that you’ve got your environment variables set up, create a new directory named src, where your source files will live. Inside that directory, create a new file called config.ts that will allow your API to access these variables:

mkdir src
cd src
touch config.ts

Adding the following code to src/config.ts:

import * as dotenv from 'dotenv';
dotenv.config();

const config = {
  TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID,
  TWILIO_API_KEY: process.env.TWILIO_API_KEY,
  TWILIO_API_SECRET: process.env.TWILIO_API_SECRET
};

export default config;

Now you’re ready to configure your TypeScript setup.

Set your TypeScript configuration with a tsconfig.json file

Move up one directory, if you’re still inside the src folder. Then run the following command to create a tsconfig.json file:

node_modules/.bin/tsc --init

If you review this file in your code editor, you’ll find that there are a ton of possible settings for your TypeScript compiler there. To learn more about these settings, you can check out the documentation for TSConfig.

You’ll only need to set a few of the settings for this project, so replace the contents of tsconfig.json with the lines below:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
  }
}

The most important settings to notice here are:

  • target: This sets the particular version of JavaScript you want to target in your project. For this project we’re targeting ES5.
  • rootDir and outDir: In this project, the compiler will look for TypeScript files in src (rootDir) and output the processed files to the build directory (outDir).

Okay, now that you’ve got your project set up, it’s time to start building the video API!

Build the video API

Configure the Express server

Start by setting up your Express server and getting it running. Inside the src directory, create a file called index.ts.

cd src
touch index.ts

Create another directory inside src called routes. Inside this folder, create a file called room.ts. This is where you'll build the routes for your video room API.

mkdir routes
cd routes
touch room.ts

When you’ve completed this step, you should have a file structure like this:

├── node_modules
│   └──  ... (long list of modules here)
├── .env
├── .gitignore
├── package-lock.json
├── package.json
├── src
│   ├── config.ts
│   ├── index.ts
│   └── routes
│       └── room.ts
└── tsconfig.json

Open src/index.ts in your code editor and add the following code to create your Express server:

import express from 'express';

const app = express();

app.use(express.json());

app.listen(5000, () => {
  console.log('Express server listening on port 5000');
});

Locate the scripts section of your package.json file. Add a comma after the test script, then add the following start script on the line below that:

"start": "node-dev src/index.ts"

It’s time to run your server! From the command line, run the following command:

npm run start

You should notice log records similar to these printed in your terminal:

> express-video-api@1.0.0 start
> node-dev src/index.ts

Express server listening on port 5000

You may or may not have a longer file path before express-video-api@1.0.0 start, but as long as the Express server listening on port 5000 log is present, you’re ready for the next step.

Because you're using node-dev here, the server will automatically reload when you change the code in your *.ts files. So there's no need to worry about stopping and restarting the server every time you make a small change. It’ll happen automatically! Isn’t that cool?

Leave the server running in one tab of your terminal window. Open another tab to continue on with the rest of the tutorial.

Build the routes

To manage your video rooms, you'll want a way to create a new room, list all of the in-progress rooms, get a specific room by its unique ID, and change a room's status from in-progress to completed when it's time for the video call to end. In this tutorial, you'll be using the Express router to handle these requests. The following sections will walk you through building each of these routes in your API.

Initialize the Twilio client

Begin by initializing the Twilio client in src/index.ts. Just below your import for express, add the following lines of code:

import config from './config';
import { Twilio } from 'twilio';

// Initialize Twilio client
const getTwilioClient = () => {
  if (!config.TWILIO_ACCOUNT_SID || !config.TWILIO_API_KEY || !config.TWILIO_API_SECRET) {
    throw new Error(`Unable to initialize Twilio client`);
  }
  return new Twilio(config.TWILIO_API_KEY, config.TWILIO_API_SECRET, { accountSid: config.TWILIO_ACCOUNT_SID })
}

export const twilioClient = getTwilioClient();

This will create a new Twilio client using the environment variables in your config file, and then export that twilioClient so that it can be used across your API.

Create a new video room

The next step is setting up a new router. Open src/routes/room.ts and add these imports at the top of the file:

import { Router } from 'express';
import { twilioClient } from '../index';

Then, create an interface to work with room data. Interfaces are TypeScript’s way of defining the syntax that a particular data type—in this case, an object—should conform to.

Add the following to the room.ts file:

interface Room {
  name: string;
  sid: string;
}

Next, create a new router called roomsRouter below the newly defined interface.

const roomsRouter = Router();

Then, export roomsRouter so it can be used elsewhere in your API.

export default roomsRouter;

Your file should look like this now:

import { Router } from 'express';
import { twilioClient } from '../index';

interface Room {
  name: string;
  sid: string;
}

const roomsRouter = Router();

export default roomsRouter;

It's time to add your first route. In room.ts and right above the export line, add the following code:

/**
 * Create a new video room
 */

roomsRouter.post('/create', async (request, response, next) => {

  // Get the room name from the request body.
  // If a roomName was not included in the request, default to an empty string to avoid throwing errors.
  const roomName: string = request.body.roomName || '';

  try {
    // Call the Twilio video API to create the new room.
    const room = await twilioClient.video.rooms.create({
        uniqueName: roomName,
        type: 'group'
      });

    // Return the room details in the response.
    return response.status(200).send(room)

  } catch (error) {
    // If something went wrong, handle the error.
    return response.status(400).send({
      message: `Unable to create new room with name=${roomName}`,
      error
    });
  }
});

If you haven't worked with async/await before, or if you want a refresher, check out the explanations in this other great tutorial about async and await in JavaScript.

When you POST your request to this /create endpoint with a room name in the request body, the code will call the Twilio Video Rooms API to create the video room. The API will respond with a 200 OK response along with the details of the created room, which you will be able to find in your terminal window. Once you create a room, it will stay active for 5 minutes even if no one joins the video chat.

In the case that something goes wrong, the error is caught in the catch statement, and a helpful error message will show up in your terminal.

Now that you've got a new route in your router, make sure to include the router in your main src/index.ts file so that the file looks like the example code below.

import express from 'express';
import config from './config';
import roomsRouter from './routes/room'; /* NEW */
import { Twilio } from 'twilio';

// Initialize Twilio client
const getTwilioClient = () => {
  if (!config.TWILIO_ACCOUNT_SID || !config.TWILIO_API_KEY || !config.TWILIO_API_SECRET) {
    throw new Error(`Unable to initialize Twilio client`);
  }
  return new Twilio(config.TWILIO_API_KEY, config.TWILIO_API_SECRET, { accountSid: config.TWILIO_ACCOUNT_SID })
}

export const twilioClient = getTwilioClient();

const app = express();

app.use(express.json());

// Forward requests for the /rooms URI to our rooms router
app.use('/rooms', roomsRouter); /* NEW */

app.listen(5000, () => {
  console.log('Express server listening on port 5000');
});

Now seems like a great time to test your API. I personally like HTTPie, but you can also use cURL if you’re not up for installing a new package.

In your second terminal tab, try calling the /create endpoint with HTTPie by running the command below:

http POST localhost:5000/rooms/create roomName="Room 1"

Or, if you prefer to use a cURL request, run the following command:

curl -X POST localhost:5000/rooms/create \
    -d '{"roomName":"Room 1"}' \
    -H "Content-Type: application/json"

When you make your request to the /create endpoint, a response should appear in your terminal similar to the one below:

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 864
Content-Type: application/json; charset=utf-8
Date: Thu, 18 Mar 2021 19:17:34 GMT
ETag: W/"<ETAG>"
Keep-Alive: timeout=5
X-Powered-By: Express

{
    "accountSid": "<ACCOUNT_SID_HERE>",
    "dateCreated": "2021-03-18T19:17:34.000Z",
    "dateUpdated": "2021-03-18T19:17:34.000Z",
    "duration": null,
    "enableTurn": true,
    "endTime": null,
    "links": {
        "participants": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>/Participants",
        "recording_rules": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>/RecordingRules",
        "recordings": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>/Recordings"
    },
    "maxConcurrentPublishedTracks": null,
    "maxParticipants": 50,
    "mediaRegion": "us1",
    "recordParticipantsOnConnect": false,
    "sid": "<ROOM_SID_HERE>",
    "status": "in-progress",
    "statusCallback": null,
    "statusCallbackMethod": "POST",
    "type": "group",
    "uniqueName": "Room 1",
    "url": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>",
    "videoCodecs": [
        "VP8",
        "H264"
    ]
}

If you check the uniqueName field for this created room, it should match the "Room 1" that you entered above. This means that your video room is created and alive online!

List active video rooms

Now that you are able to create new video rooms, wouldn’t it be great to get a list of all the in-progress rooms?

This next route has a similar structure to the one you wrote in the previous section. In this one, however, you’ll use the interface you created earlier in the tutorial to return only select data about each room—the room’s unique identifier (sid) and its name (uniqueName).

Add the following code below the /create route and just above the export line:

/**
* List active video rooms
*
* You can also select other ways to filter/list!
* For the purposes of this tutorial, though, only the in-progress rooms are returned.
*/
roomsRouter.get('/', async (request, response, next) => {
  try {
    // Get the last 20 rooms that are still currently in progress.
    const rooms = await twilioClient.video.rooms.list({status: 'in-progress', limit: 20});

    // If there are no in-progress rooms, return a response that no active rooms were found.
    if (!rooms.length) {
      return response.status(200).send({
        message: 'No active rooms found',
        activeRooms: [],
      });
    }

    // If there are active rooms, create a new array of `Room` objects that will hold this list.
    let activeRooms: Room[] = [];

    // Then, for each room, take only the data you need and push it into the `activeRooms` array.
    rooms.forEach((room) => {
      const roomData: Room = {
        sid: room.sid,
        name: room.uniqueName
      }

      activeRooms.push(roomData);
    });

    return response.status(200).send({activeRooms});

  } catch (error) {
    return response.status(400).send({
      message: `Unable to list active rooms`,
      error
    });
  }
});

Using the HTTPie or cURL commands from the previous section, create new video rooms called “Room 2” and “Room 3”.

When you've created the new rooms, try listing these in-progress rooms by running the following command with HTTPie:

http GET localhost:5000/rooms/

If you’re using cURL, you can run this command:

curl -X GET localhost:5000/rooms/

The response will contain a list of the active rooms, as shown below:

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 139
Content-Type: application/json; charset=utf-8
Date: Thu, 18 Mar 2021 20:42:57 GMT
ETag: W/"<ETAG>"
Keep-Alive: timeout=5
X-Powered-By: Express

{
    "activeRooms": [
        {
            "name": "Room 3",
            "sid": "<ONE_ROOM_SID_HERE>"
        },
        {
            "name": "Room 2",
            "sid": "<ANOTHER_ROOM_SID_HERE>"
        }
    ]
}

If there are no active rooms, you’ll see the message No active rooms found.

Get details about an existing video room

Now that you can list the video rooms, what if you wanted to get details about a specific room? It’s time to add a route that will return that room for you.

In the room.ts file, below the route for listing rooms, add the following code to retrieve a room by its unique identifier (sid):

/**
 * Get a specific room by its SID (unique identifier)
 *
 * It is also possible to get rooms by name, but this only works for in-progress rooms.
 * You can use this endpoint to get rooms of any status!
 */

roomsRouter.get('/:sid', async (request, response, next) => {
  const sid: string = request.params.sid;

  try {
    // Call the Twilio video API to retrieve the room you created
    const room = await twilioClient.video.rooms(sid).fetch();

    return response.status(200).send({room});

  } catch (error) {
    return response.status(400).send({
      message: `Unable to get room with sid=${sid}`,
      error
    });
  }
});

Create a new video room called "Room 4" using HTTPie or cURL. Take note of the room's sid from the response in your terminal. Then try calling the new endpoint to get this room by its sid.

Here's the command if you are using HTTPie:

http GET localhost:5000/rooms/<YOUR_ROOM_SID_HERE>

Here's the cURL command:

curl -X GET localhost:5000/rooms/<YOUR_ROOM_SID_HERE>

The response in your terminal will contain the details of the selected room:

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 873
Content-Type: application/json; charset=utf-8
Date: Thu, 18 Mar 2021 19:42:53 GMT
ETag: W/"<ETAG>"
Keep-Alive: timeout=5
X-Powered-By: Express

{
    "room": {
        "accountSid": "<ACCOUNT_SID_HERE>",
        "dateCreated": "2021-03-18T19:42:33.000Z",
        "dateUpdated": "2021-03-18T19:42:33.000Z",
        "duration": null,
        "enableTurn": true,
        "endTime": null,
        "links": {
            "participants": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>/Participants",
            "recording_rules": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>/RecordingRules",
            "recordings": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>/Recordings"
        },
        "maxConcurrentPublishedTracks": null,
        "maxParticipants": 50,
        "mediaRegion": "us1",
        "recordParticipantsOnConnect": false,
        "sid": "<ROOM_SID_HERE>",
        "status": "in-progress",
        "statusCallback": null,
        "statusCallbackMethod": "POST",
        "type": "group",
        "uniqueName": "Room 4",
        "url": "https://video.twilio.com/v1/Rooms/<ROOM_SID_HERE>",
        "videoCodecs": [
            "VP8",
            "H264"
        ]
    }
}

Close a video room

To complete your API, add a route for closing a video room. This will end the video chat and also disconnect any participants who are still connected to the room. With the Twilio Video Rooms API, all you need to do to close a video room is change its status from in-progress to completed.

For this route, you'll pass in the sid of the room you want to change. Add the following code just above the export line in room.ts:

/**
* Complete a room
*
* This will update the room's status to `completed` and will end the video chat, disconnecting any participants.
* The room will not be deleted, but it will no longer appear in the list of in-progress rooms.
*/

roomsRouter.post('/:sid/complete', async (request, response, next) => {
  // Get the SID from the request parameters.
  const sid: string = request.params.sid;

  try {
    // Update the status from 'in-progress' to 'completed'.
    const room = await twilioClient.video.rooms(sid).update({status: 'completed'})

    // Create a `Room` object with the details about the closed room.
    const closedRoom: Room = {
      sid: room.sid,
      name: room.uniqueName,
    }

    return response.status(200).send({closedRoom});

  } catch (error) {
    return response.status(400).send({
      message: `Unable to complete room with sid=${sid}`,
      error
    });
  }
});

Create another video room called “Room 5”. Once you’ve created this room, take note of the room’s sid in your terminal. You’ll also notice that the room has a status of in-progress.

When you’re ready to try closing the room, run the following command with HTTPie:

http POST localhost:5000/rooms/<ROOM_SID_HERE>/complete

If using cURL, run this command instead:

curl -X POST localhost:5000/rooms/<ROOM_SID_HERE>/complete

The response in your terminal will let you know that the room is now closed:

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 75
Content-Type: application/json; charset=utf-8
Date: Thu, 18 Mar 2021 22:11:10 GMT
ETag: W/"<ETAG>"
Keep-Alive: timeout=5
X-Powered-By: Express

{
    "closedRoom": {
        "name": "Room 5",
        "sid": "<ROOM_SID_HERE>"
    }
}

What to build next with your video API and Twilio Programmable Video

Being able to create and manage video rooms programmatically is a great step towards building the video-enabled app of your dreams. With this as a starting point, you could build all sorts of ideas, from EdTech to social media to telemedicine.

If you want to take a look at the code from this tutorial in its entirety, you can find it here on GitHub, on the getting-started branch.

In future tutorials, you’ll have a chance to build on this API to create other interesting projects. Or perhaps you already have an idea in mind and are ready to give it a go. I can’t wait to see what you build!

Mia Adjei is a Software Developer on the Developer Voices team. They love to help developers build out new project ideas and discover aha moments. Mia can be reached at madjei [at] twilio.com.