Home

 / Blog / 

Building a sample backend server for your 100ms app with Node.js

Building a sample backend server for your 100ms app with Node.js

February 22, 202317 min read

Share

100ms-sample-backend-nodejs-cover.png

If you’re someone who has just start building with 100ms live video SDKs, you might have noticed that there are certain things you can’t do with just the SDK. From creating a new room, to generating an Auth Token to join a room, the Dashboard is your only friend—or so you thought.

Any application built using 100ms SDK consists of 2 components:

  1. Client - The frontend app that uses the 100ms SDK (Android, iOS, Web, React Native, Flutter) for the end-users.
  2. Server - The backend that uses 100ms REST APIs to create rooms, trigger recording or Live streaming, access webhook events.

Everything you can do with the 100ms REST APIs (Server) can be done in your Dashboard. But, the Dashboard being your only option is not recommended in production. Before we start, here are some 100ms terminologies you should be familiar with:

  1. Room - A room is a virtual space where one or more peers communicate with each other. Rooms map to real-world entities: a classroom, tele-health consultation, or a webinar. You can imagine a physical room in a building that is open at all times, where anyone authorized can enter and leave.
  2. Auth Token - Used to authenticate and allow end-users (peers) to join 100ms rooms. This can be considered an entry pass that authorizes a peer to enter a specific room.
  3. Session - Session is a single continuous call in a room. A session begins when the first peer joins an empty room and ends when the last peer leaves it. A single room can have multiple sessions.

In this guide, we’ll build a sample backend app in Node.js that uses 100ms REST APIs to do the following:

  1. Create a new Room
  2. Generate Auth Token for joining a Room
  3. Get usage analytics for the latest Session in the Room

Now that we know what we’ll be building, let’s begin!

Initialize Node.js app

Let us start by creating a new project directory 100ms-sample-token-server-nodejs. Then, the following command is used to create package.json with configurations needed for a Node.js App.

npm init

On executing the command, when prompted for Entry point, type src/app.js.

Setting up Express with Node.js

We’ll be using Express.js for serving the HTTP endpoints. To use the Express framework, we need to install it as a dependency first.

npm i --save express

Now create a file called app.js inside a new directory called src and create an express app inside it by using the following code.

let express = require('express');
let app = express();
app.use(express.json()); // for parsing request body as JSON
let port = process.env.PORT || 3000;

app.listen(port, () => {
    console.log(`Token server started on ${port}!`);
});

Installing Additional Dependencies

Install the following dependencies,

npm i axios dotenv jsonwebtoken moment uuid4

Here’s what we’ll be using these dependencies for:

  1. axios → for making API calls to 100ms servers
  2. dotenv → for loading environmental variables from .env file
  3. jsonwebtoken → for generating auth and management tokens (JWT tokens)
  4. uuid4 → for generating random UUID
  5. moment → for time related operations

Configuring Environment Variables

Now, login to your 100ms Dashboard and go to the Developer section. You will find 2 values that you’d require for both generating the Auth Token and making 100ms REST API calls.

Now, copy the values of “App Access Key” and “App Secret” into a file called .env.

APP_ACCESS_KEY=<YOUR_APP_ACCESS_KEY>
APP_SECRET=<YOUR_APP_SECRET>

Setting up Token Service

We will be dealing with two kinds of tokens here:

  1. Auth Token - a token that would enable a client to join a room.
  2. Management Token - a token that would enable the app(this server) to communicate with 100ms REST APIs.

To generate and manage these tokens, we will now create a service class TokenService inside the src/services/TokenService.js file.

In order to generate a token, we will sign the payload that contains the App Access Key along with several other configurations, using the JWT mechanism with the App Secret as key. First, let us create 2 private methods—one to sign the payload using JWT and the other to check if an existing token has expired.

If you’re a TypeScript person, the symbol # is used to make a class member private in JavaScript. It is the equivalent of using the keyword private in TS.

let jwt = require('jsonwebtoken');
let uuid4 = require('uuid4');

// A service class for Token generation and management
class TokenService {
    static #app_access_key = process.env.APP_ACCESS_KEY;
    static #app_secret = process.env.APP_SECRET;
    #managementToken;
    constructor() {
        this.#managementToken = this.getManagementToken(true);
    }

    // A private method that uses JWT to sign the payload with APP_SECRET
    #signPayloadToToken(payload) {
        let token = jwt.sign(
            payload,
            TokenService.#app_secret,
            {
                algorithm: 'HS256',
                expiresIn: '24h',
                jwtid: uuid4()
            }
        );
        return token;
    }

    // A private method to check if a JWT token has expired or going to expire soon
    #isTokenExpired(token) {
        try {
            const { exp } = jwt.decode(token);
            const buffer = 30; // generate new if it's going to expire soon
            const currTimeSeconds = Math.floor(Date.now() / 1000);
            return !exp || exp + buffer < currTimeSeconds;
        } catch (err) {
            console.log("error in decoding token", err);
            return true;
        }
    }
}

module.exports = {TokenService};

Now, in order to generate these tokens, we’ll add the following the methods:

  1. getManagementToken() - Checks if the existing Management Token has expired. Generates a new token in that case and returns it.
  2. getAuthToken() - Generates a new Auth Token using the room id, user id and role and returns it.
class TokenService {
...
		// Generate new Management token, if expired or forced
	  getManagementToken(forceNew) {
        if (forceNew || this.#isTokenExpired(this.#managementToken)) {
            let payload = {
                access_key: TokenService.#app_access_key,
                type: 'management',
                version: 2,
                iat: Math.floor(Date.now() / 1000),
                nbf: Math.floor(Date.now() / 1000)
            };
            this.#managementToken = this.#signPayloadToToken(payload);
        }
        return this.#managementToken;
    }

    // Generate new Auth token for a peer
    getAuthToken({ room_id, user_id, role }) {
        let payload = {
            access_key: TokenService.#app_access_key,
            room_id: room_id,
            user_id: user_id,
            role: role,
            type: 'app',
            version: 2,
            iat: Math.floor(Date.now() / 1000),
            nbf: Math.floor(Date.now() / 1000)
        };
        return this.#signPayloadToToken(payload);
    }
}

Setting up API Service

To make REST API calls to 100ms servers from this app, we’ll use a HTTP client called axios. We will create a service class APIService inside the src/services/APIService.js to handle all the REST api calls with a configured axios instance.

We will use the constructor and a private method configureAxios() to create and configure an axios instance:

  • With the baseURL set to "https://api.100ms.live/v2"
  • Uses the Management token from the TokenService for Bearer Authorization
  • Retries a failed request with a newly generated Management token
const axios = require('axios').default;

// A service class for all REST API operations
class APIService {
    #axiosInstance;
    #tokenServiceInstance
    constructor(tokenService) {
        // Set Axios baseURL to 100ms API BaseURI
        this.#axiosInstance = axios.create(
            {
                baseURL: "https://api.100ms.live/v2",
                timeout: 3 * 60000
            });
        this.#tokenServiceInstance = tokenService;
        this.#configureAxios();
    }

    // Add Axios interceptors to process all requests and responses
    #configureAxios() {
        this.#axiosInstance.interceptors.request.use((config) => {
            // Add Authorization on every request made using the Management token
            config.headers = {
                Authorization: `Bearer ${this.#tokenServiceInstance.getManagementToken()}`,
                Accept: "application/json",
                "Content-Type": "application/json",
            };
            return config;
        },
            (error) => Promise.reject(error));
        this.#axiosInstance.interceptors.response.use((response) => {
            return response;
        },
            (error) => {
                console.error("Error in making API call", { response: error.response?.data });
                const originalRequest = error.config;
                if (
                    (error.response?.status === 403 || error.response?.status === 401) &&
                    !originalRequest._retry
                ) {
                    console.log("Retrying request with refreshed token");
                    originalRequest._retry = true;

                    // Force refresh Management token on error making API call
                    this.axios.defaults.headers.common["Authorization"] = "Bearer " + this.#tokenServiceInstance.getManagementToken(true);
                    try {
                        return this.axios(originalRequest);
                    } catch (error) {
                        console.error("Unable to Retry!");
                    }
                }
                return Promise.reject(error);
            }
        );
    }
}

module.exports = {APIService};

We will also add the methods get() and post() that uses this configured axios instance to make requests and return the data from the response. These are the methods we will use to make the 100ms REST API calls.

class APIService {
...
		// A method for GET requests using the configured Axios instance
    async get(path, queryParams) {
        const res = await this.#axiosInstance.get(path, { params: queryParams });
        console.log(`get call to path - ${path}, status code - ${res.status}`);
        return res.data;
    }

    // A method for POST requests using the configured Axios instance
    async post(path, payload) {
        const res = await this.#axiosInstance.post(path, payload || {});
        console.log(`post call to path - ${path}, status code - ${res.status}`);
        return res.data;
    }
}

Create a New Room

Now, create a POST request handler for the route /create-room to create a new Room using the 100ms REST APIs. And the arguments for creating a room, like name and description of the room are specified in the body of the request.

The client can use this endpoint to create a new room as and when needed. It is the responsibility of the client to store the generated room id, to use it in the subsequent requests to generate Auth Token and the usage analytics.

// Create a new room, either randomly or with the requested configuration
app.post('/create-room', async (req, res) => {
    const payload = {
        "name": req.body.name,
        "description": req.body.description,
        "template_id": req.body.template_id,
        "region": req.body.region
    };

    try {
        const resData = await apiService.post("/rooms", payload);
        res.json(resData);
    } catch (err) {
        console.error(err);
        res.status(500).send("Internal Server Error");
    }
});

Serving the Auth Token

Now back to the src/app.js, create an instance of both TokenService and APIService to use in the request handlers.

Create a POST request handler for the route /auth-token to serve the Auth Token to the clients. Use the getAuthToken() method from the TokenService instance to generate one and send it as response.

The client can use the room id of the newly generated room and make this request. Once the Auth Token has been generated, the client can use it to join that room using the 100ms SDK.

...

require('dotenv').config();
const moment = require('moment');
const { APIService } = require('./services/APIService');
const { TokenService } = require('./services/TokenService');

const tokenService = new TokenService();
const apiService = new APIService(tokenService);

// Generate an auth token for a peer to join a room
app.post('/auth-token', (req, res) => {
    try {
        const token = tokenService.getAuthToken({ room_id: req.body['room_id'], user_id: req.body['user_id'], role: req.body['role'] });
        res.json({
            token: token,
            msg: "Token generated successfully!",
            success: true,
        });
    } catch (error) {
        res.json({
            msg: "Some error occured!",
            success: false,
        });
    }
});

Get usage analytics for the latest Session in the Room

Finally, create a GET request handler for the route /session-analytics that does the following:

  1. Fetch all the sessions for a specific room.
  2. Processes the session details of the latest session among them, to generate usage analytics.

Here, we take the example of Attendance. The session details are fetched from the API call based on room id and then the response is used to calculate the following:

  • Individual participants' duration in the call
  • Aggregated participants' duration
  • Total session duration
// Get usage analytics for the latest Session in a Room
app.get('/session-analytics-by-room', async (req, res) => {
    try {
        const sessionListData = await apiService.get("/sessions", { room_id: req.query.room_id });
        if (sessionListData.data.length > 0) {
            const sessionData = sessionListData.data[0];
            console.log(sessionData);

            // Calculate individual participants' duration
            const peers = Object.values(sessionData.peers);
            const detailsByUser = peers.reduce((acc, peer) => {
                const duration = moment
                    .duration(moment(peer.left_at).diff(moment(peer.joined_at)))
                    .asMinutes();
                const roundedDuration = Math.round(duration * 100) / 100;
                acc[peer.user_id] = {
                    "name": peer.name,
                    "user_id": peer.user_id,
                    "duration": (acc[peer.user_id] || 0) + roundedDuration
                };
                return acc;
            }, {});
            const result = Object.values(detailsByUser);
            console.log(result);

            // Calculate aggregated participants' duration
            const totalDuration = result
                .reduce((a, b) => a + b.duration, 0)
                .toFixed(2);
            console.log(`Total duration for all peers: ${totalDuration} minutes`);

            // Calculate total session duration
            const sessionDuration = moment
                .duration(moment(sessionData.updated_at).diff(moment(sessionData.created_at)))
                .asMinutes()
                .toFixed(2);
            console.log(`Session duration is: ${sessionDuration} minutes`);

            res.json({
                "user_duration_list": result,
                "session_duration": sessionDuration,
                "total_peer_duration": totalDuration
            });
        } else {
            res.status(404).send("No session found for this room");
        }
    } catch (err) {
        console.error(err);
        res.status(500).send("Internal Server Error");
    }
});

Deploy on Render

If you want to try it out without much trouble, you can deploy this app on Render for free. Make sure to specify the environment variables when asked.

Deploy to Render

You can check out the complete source code here at 100ms-sample-backend-nodejs. Don’t forget to star the repo if it was useful to you!

Engineering

Share

Related articles

See all articles