Slack apps are awesome - they keep us entertained with GIFs, help us look up words from the dictionary — they even launch meetings!
We recently built a simple Slack app. The app allows users to type /eval <JavaScript code> directly in Slack that evaluates JavaScript code and prints the output directly in a Slack response. Try it out!
In this blog post, we will show you exactly how we did it. We will demonstrate how you can easily build, deploy and distribute similar Slack apps for free, leveraging the power of serverless on Now.

We start by creating a new Slack app. After naming it, we customize it with our choice of color and icon.

When creating a new app, Slack asks for an app name and a default workspace.

On the app’s Basic Information page, we find that Slack allows us to add features to our app.

Add a Slash Command

Let’s create a new Slash Command called /eval. Each time someone types in /eval <code>, Slack will send a POST request to a Request URL that we specify. We need to process the request and respond with the result we get from executing the code.
We’ll work on the request handler shortly. To keep moving forward for now, let’s set an arbitrary URL.
Since we don’t know what code to expect in the incoming request, let’s start by exporting a function that will execute our code in a secure sandboxed environment. We’ll use vm2 for this.
const { VM } = require('vm2')

module.exports = (str, timeout = 1000) => {
  const sandboxedEnvironment = new VM({
    sandbox: {},

lib/eval.js to execute code in a sandboxed environment

This represents our core operation. Now we need to write code that will accept and parse the request from Slack, run our core operation, and give it back to Slack. We’ll use a lambda for that.
Lambdas contain code that can be executed on-demand and scaled automatically.
Based on the nodejs “Hello World” lambda example, here’s our rough workflow. We will export a function that receives the request and response objects like so:
module.exports = (req, res) => {
  // Parse code received through req
  // Pass code to function imported through eval
  // Send back result or errors to Slack

Draft of overall flow on index.js

We import the text helper from micro to help us with parsing the incoming request body, and also lib/eval from earlier to help us execute our code safely.
const { text } = require('micro')
const { parse } = require('querystring')
const evaluateIncomingJS = require('./lib/eval')

module.exports = async (req, res) => {
  // Parse code received through req
  const body = parse(await text(req))
  let result, attachments

  try {
    // Pass code to function imported through eval
    result = evaluateIncomingJS(body.text, 2500)
  } catch (error) {
    // Capture any errors
    result = error.message
    attachments = [{ text: error.stack }]

  const message = '`' + body.text + '`: ' + result
  const response_type = 'in_channel'

  res.writeHead(200, { 'Content-Type': 'application/json' })
  // Create response object and send result back to Slack
  res.end(JSON.stringify({ response_type, text: message, attachments }))

index.js updated for handling Slack's HTTP POST request

We now have everything we need to see the app in action within our Slack — we just need to deploy it!
We will use the @now/node builder for our index.js lambda file.
  "name": "eval",
  "version": 2,
  "builds": [
      "src": "index.js",
      "use": "@now/node"

Barebones config within now.json

Deployment is rather straightforward.
$ now
Now provides us with a unique URL. As a final step, we update the Request URL on our Slack app to use the new URL.

The Request URL can be found on the Basic Information page of the Slack app, within Slash Commands

With that, we should now be able to test our app in action!

Try out the app by entering /eval 2 + 2 within Slack

We find that the result shows as expected. Errors are elegantly handled too, including stack traces as attachments just as we wanted! 🎉
The easiest way to allow others to install an app on their Slack workspace is the Add to Slack button. To set up the button, we need to prepare our app for an OAuth flow.
This involves 5 steps:
  1. Clicking Add to Slack redirects the Slack user to an OAuth request page
  2. Once the user accepts the OAuth request, Slack redirects them to a Redirect URL we specify, along with a temporary code
  3. We exchange the code for an access_token by making a POST request to Slack's oauth.access endpoint
  4. We use the access_token to obtain the user's Slack domain, by making a POST request to Slack's auth.test endpoint
  5. We redirect the user to their Slack domain
Like last time, we will use another lambda to handle the OAuth flow. We begin by drafting the flow. Once deployed, we can set that as our Redirect URL within Slack.
module.exports = (req, res) => {
  // Extract code received on the request url
  // Compose authHeader by encoding the string ${client_id}:${client_secret}
  // Hit oauth.access for access_token
  // Hit auth.test for slack domain
  // Send redirect response to slack domain

Draft of overall flow on oauth.js

We'll use node-fetch to make our POST requests. We don't want to hardcode our client secrets in the code, so we'll make use of Now environment variables. We can set their values at deploy time.
const fetch = require('node-fetch')
const { parse, stringify } = require('querystring')

module.exports = async (req, res) => {
  // Extract code received on the request url
  const urlQueryString = req.url.replace(/^.*?/, '')
  const code = parse(urlQueryString).code

  // Compose authHeader by encoding the string ${client_id}:${client_secret}
  const client_id = process.env.SLACK_CLIENT_ID
  const client_secret = process.env.SLACK_CLIENT_SECRET
  const Authorization =
    'Basic ' + Buffer.from(`${client_id}:${client_secret}`).toString('base64')

  // Hit oauth.access for access_token
  const oauthAccess = await fetch('', {
    method: 'POST',
    body: stringify({ code }),
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
  const { access_token } = await oauthAccess.json()

  // Hit auth.test for slack domain
  const authTest = await fetch('', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${access_token}`
  const { url: slackUrl } = await authTest.json()

  // Send redirect response to slack domain
  res.writeHead(302, 'Redirect', { Location: slackUrl })

ouath.js ready to handle Slack's OAuth flow

Next, we update now.json to include build information for oauth.js, as well as the secrets.
"builds": [{
  "src": "oauth.js",
  "use": "@now/node"
"env": {
  "SLACK_CLIENT_ID": "@slack-client-id",
  "SLACK_CLIENT_SECRET": "@slack-client-secret"

Build config for oauth.js

This time before we deploy, we set our environment variables with now secret.
$ now secret add slack-client-id xxxx
$ now secret add slack-client-secret xxxx
$ now

can be found on the Slack app's Basic Information page

Finally, we add the Redirect URL to our app's OAuth and Permissions page. Do not forget to hit Save URLs!

Each time a user goes through the OAuth flow, they invoke theoauth.js lambda to complete the process.

All that remains now is to embed the Add to Slack button on our app's landing page. Slack also provides us with a Shareable URL that can be used by anybody to add our app onto their Slack workspace.
Both can be found on the app's Manage Distribution page.

To create a custom button, we simply need to link to the Share URL that Slack makes available

Once done, our app is ready for every Slack user to enjoy at the click of a button! ✨
At ZEIT, we are always looking to enable our developer community towards greater productivity. One way we do that is by showcasing simple yet powerful usages of our platform.
We hope you enjoyed reading our exploration of creating Serverless Slack apps with Now as much as we enjoyed writing it. You can add the Serverless Eval app to your Slack org through its website. We've made the source code publicly available on GitHub.
We are very excited to see what this inspires you to create. Feel free to write a guide on your experience! Catch us on Twitter and let us know!