Intro
I recently worked on a side project that involved building custom chat commands for a bot on Twitch.tv. The commands themselves required a lot of heavy lifting with the Google Sheets API - something that was the perfect candidate for a Node.js server.
This wasn't the first time I've done custom bot work on Twitch or Discord. For previous projects, I've always spun up a custom server to manage the bot that was then deployed (at cost) to Heroku. However, after a few hours of initial work on this new project, I discovered it would be much easier to tackle the bot commands using modern serverless technologies. After all, each bot command is just a function.
In theory, this could be done using anything that lets you easily host an API endpoint without a server. I chose Next.js because a lot of similar non-bot-related feature work lived in the same Next.js project.
How it works
- 🤖 Your Twitch channel is running Nightbot, which supports custom API-based "UrlFetch" commands. Nightbot is free to use and takes less than 20 seconds to set up on your channel.
- 👨💻 You use Next.js's API routes support to build serverless backend microservice functions.
- 😎 You deploy your project to Vercel or Netlify for free.
- 📡 You create a custom command with Nightbot leveraging UrlFetch and your newly-deployed API route.
🔧 Let's build it
Set up a fresh Next.js project
Let's create a new Next.js project. I'll be using TypeScript for this project, but this can easily be adapted to work with JavaScript instead.
In your terminal in the directory you'd like to create the project, run:
npx create-next-app --example with-typescript
OR
yarn create next-app --example with-typescript
After a few minutes, your project should be ready to go, and a dev server can be started with npm run dev
or yarn dev
.
Add a new API route
Creating serverless functions in Next.js is so easy it feels like cheating. You should have a pages folder in your project. Create an api folder inside pages and within it create a new file: ping.ts. Your file structure should look something like this (I have not modified the TypeScript example project):
With your dev server running at yarn dev
, http://localhost:3000/api/ping now automagically maps to your ping.ts file! But it isn't doing anything yet.
Make the API route useful for Nightbot
Our custom chat command will be very simple. There will be no heavy lifting involved. For this article, we want the command to say hello, print the initiator's username, and print the current channel. Like so:
Let's get coding. Open up ping.ts and paste this content in:
// ping.ts
import { NextApiRequest, NextApiResponse } from 'next';
export default async function (req: NextApiRequest, res: NextApiResponse) {
res.status(200).send('Hello!');
}
With your local dev server running (npm run dev
or yarn dev
), if you visit localhost:3000/api/ping, you should see "Hello!" printed to the screen. Cool!
Some things to note if this is your first Next.js rodeo:
-
req
andres
may look like conventional Express.js request and response arguments, but they are not.NextApiRequest
andNextApiResponse
are Express-like. Docs here on response helpers might be useful. - If all of this looks like moon language, the Next.js API routes documentation is a pretty good first start.
- By default, Nightbot expects a plain-text response. JSON is supported, but beyond the scope of this article.
Alright, we're printing "Hello" to the screen, but what about the username and the current channel? When Nightbot sends an API request, it sends along headers with all that metadata too! Info on these headers can be found on the UrlFetch docs page:
We're specifically interested in Nightbot-User
and Nightbot-Channel
. Nightbot sends data in these headers along as query strings, like this:
req.headers['nightbot-channel'] =
'name=kongleague&displayName=KongLeague&provider=twitch&providerId=454709668';
req.headers['nightbot-user'] =
'name=wescopeland&displayName=WesCopeland&provider=twitch&providerId=52223868&userLevel=moderator'
We can use JavaScript's built-in URLSearchParams
constructor to parse these pretty easily. Add these functions to your ping.ts file:
// somewhere in ping.ts
const parseNightbotChannel = (channelParams: string) => {
const params = new URLSearchParams(channelParams);
return {
name: params.get('name'),
displayName: params.get('displayName'),
provider: params.get('provider'),
providerId: params.get('providerId')
};
};
const parseNightbotUser = (userParams: string) => {
const params = new URLSearchParams(userParams);
return {
name: params.get('name'),
displayName: params.get('displayName'),
provider: params.get('provider'),
providerId: params.get('providerId'),
userLevel: params.get('userLevel')
};
};
Updating the ping.ts API function to display the username and channel is now relatively straightforward!
// ping.ts
export default async function (req: NextApiRequest, res: NextApiResponse) {
const channel = parseNightbotChannel(
req.headers['nightbot-channel'] as string
);
const user = parseNightbotUser(req.headers['nightbot-user'] as string);
res
.status(200)
.send(
`Hello! Your username is ${user.displayName} and the current channel is ${channel.displayName}.`
);
}
✅ Let's test it
Our endpoint is built, but how would we go about building a unit test for it? You'll see below this is not too difficult. Note that Jest does not ship with new Next.js projects by default, but it is simple to set up.
Add a testing dev dependency
To make life less painful, I recommend installing the node-mocks-http
library:
npm i node-mocks-http --save-dev
OR
yarn add -D node-mocks-http
If you're a regular Express.js user, you may be familiar with testing API endpoints using supertest
. Unfortunately, supertest
cannot help us with Next.js serverless API routes.
Create the test file
Your natural inclination might be to put a ping.test.ts file in the same directory as ping.ts. This is a good pattern to follow, but due to how Next.js's folder-based routing works it isn't a great idea because Vercel will then try to deploy your tests 😱
I recommend creating a __tests__
folder at the root of your project where tests for anything inside of pages can live. Inside of __tests__
, create an api folder that contains ping.test.ts.
Write the tests
Building the test code from here is pretty straightforward:
import { createMocks } from 'node-mocks-http';
import ping from '../../pages/api/ping';
describe('Api Endpoint: ping', () => {
it('exists', () => {
// Assert
expect(ping).toBeDefined();
});
it('responds with details about the user and channel', async () => {
// Arrange
const { req, res } = createMocks({
method: 'GET',
headers: {
'nightbot-channel':
'name=kongleague&displayName=KongLeague&provider=twitch&providerId=454709668',
'nightbot-user':
'name=wescopeland&displayName=WesCopeland&provider=twitch&providerId=52223868&userLevel=moderator'
}
});
// Act
await ping(req, res);
const resData = res._getData();
// Assert
expect(resData).toContain('Your username is WesCopeland');
expect(resData).toContain('the current channel is KongLeague');
});
});
🤖 Finally, set up Nightbot
Go to the Nightbot website, sign up, and click the "Join Channel" button in your Nightbot dashboard. Nightbot will now be on your Twitch (or YouTube!) channel.
I am assuming you've deployed your Next.js project somewhere. You should be able to hit your newly created ping
route inside your browser. If you're new to this, deploying to Vercel is probably easiest for Next.js projects. It should just be a matter of signing up, pointing to your GitHub repo, and clicking Deploy.
Now that Nightbot is in your Twitch channel, go to your chat on Twitch. Create a new Nightbot command by entering in the chat:
!commands add !ping $(urlfetch https://YOUR_URL/api/ping)
After this is done, Nightbot should respond saying the command has been added. You should now be able to type "!ping" in the chat and see your API response! You're all set!
🔒 Don't forget security
Anyone can access Nightbot's list of commands for your Twitch channel just by using "!commands". Nightbot hides API route addresses, treating them like secrets or environment variables, but anyone who knows the address to one of your endpoints can mock headers and pretend to be someone they're not in Postman or Insomnia.
In other words, you need another layer of security if you want to treat the initiator of the chat command as being "authenticated".
If this is important to you (typical in advanced use cases involving things like channel points or user roles), I recommend adding code to your endpoint that ensures the API call actually came from Twitch or Nightbot itself. It's possible to check for this in the request headers of the API call.
👋 That's all!
Thanks for reading, hopefully this was helpful to someone out there! If you're interested in any of my future content, be sure to follow me here on dev.to.
Latest comments (5)
I tried following the instructions and it created my project but when I tried npm run dev I got a ton of errors:
SyntaxError: Unexpected token '??='
at wrapSafe (internal/modules/cjs/loader.js:979:16)
at Module._compile (internal/modules/cjs/loader.js:1027:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Module.load (internal/modules/cjs/loader.js:928:32)
at Function.Module._load (internal/modules/cjs/loader.js:769:14)
at Module.require (internal/modules/cjs/loader.js:952:19)
at require (internal/modules/cjs/helpers.js:88:18)
at Object. (C:\Projects\twitchbot\node_modules\next\dist\telemetry\post-payload.js:17:20)
at Module._compile (internal/modules/cjs/loader.js:1063:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! @ dev:
next
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the @ dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
Is this out of date?
Interesting and well explained
This is interesting, I didn't know this.
Awesome article! You make creating a bot for Twitch look easy!
Can you do slack next 🤣
Hey Colum,
It looks like this is relatively straightforward to do with Slack :-)
This article is a great reference that takes a very similar approach. It looks like all that needs to be done is:
... and you're done!
That looks a lot nicer than some of the tutorials from slack themselves.