Home
/ Blog /
Building a sample backend server for your 100ms app with Node.jsFebruary 22, 202317 min read
Share
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:
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:
In this guide, we’ll build a sample backend app in Node.js that uses 100ms REST APIs to do the following:
Now that we know what we’ll be building, let’s begin!
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
.
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}!`);
});
Install the following dependencies,
npm i axios dotenv jsonwebtoken moment uuid4
Here’s what we’ll be using these dependencies for:
axios
→ for making API calls to 100ms serversdotenv
→ for loading environmental variables from .env
filejsonwebtoken
→ for generating auth and management tokens (JWT tokens)uuid4
→ for generating random UUIDmoment
→ for time related operationsNow, 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>
We will be dealing with two kinds of tokens here:
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 keywordprivate
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:
getManagementToken()
- Checks if the existing Management Token has expired. Generates a new token in that case and returns it.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);
}
}
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:
"https://api.100ms.live/v2"
TokenService
for Bearer Authorizationconst 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;
}
}
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");
}
});
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,
});
}
});
Finally, create a GET request handler for the route /session-analytics
that does the following:
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:
// 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");
}
});
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.
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