Send Slack Notifications After Media Upload

Milecia

We use Slack for all kinds of tasks, like talking to coworkers and interacting with different communities. There are a lot of bots we run into that send alerts or give you other functionality like making polls. The best part is that Slack has an API that we can play with to trigger actions from a number of apps.

In this post, we're going to build a Next app that sends a slack notification once an image has been uploaded to Cloudinary. We'll go through how to set up the Slack app for your workspace and we'll implement the API call inside of our app. By the time you finish this post, you should feel more comfortable working with Next and the Slack API.

Initial project setup

1$ yarn create next-app --typescript

This command will prompt you for a project name. I've called this project slack-notifs, but feel free to name it whatever you like. We'll need to install the Slack package.

1$ yarn add @slack/web-api

If you don't already have a free Cloudinary account, sign up for one here. In the dashboard, get the cloud name for your account and the upload preset value from your settings. We'll use these a bit later to upload the images.

Setup the Slack app in your Slack workspace

First, you'll need to create a Slack app so that you can generate a token for the Next app. Log in to Slack in the browser and create a new app. Make sure you choose the right workspace and name the app Image Upload.

You'll get redirected to a page with all of the app settings we need to set up to interact with this throughout Next API. Let's start by enabling Incoming Webhooks. This is how we'll be able to post messages from the Next API. You'll be taken to another page where you'll need to add a new webhook to your workspace.

Pay extra attention to the workspace permission screen so that you know you're posting in the right channel. I have mine set to post to my direct messages to test with. After you choose the channel in your workspace, you'll be redirected back to the Incoming Webhooks page. You should test out that the app is working as you expect by using the example cURL method generated on the page.

Now you need to go to the OAuth & Permissions page in the left sidebar. This is where we set the scopes for the access token we'll use to post messages to our channel. You'll need the channels:read, chat:write, and incoming-webhook scopes on the Bot Token Scopes.

With all of this in place, we can move in the Next app and start writing some code.

Write the back-end

Let's start by creating a .env file at the root of the project to hold the credentials we need to use Slack and Cloudinary. The file should look something like this.

1SLACK_TOKEN=xoxb-your-token
2CLOUDINARY_UPLOAD_PRESET=your-upload-preset
3CLOUDINARY_CLOUD_NAME=your-cloud-name

We're going to take advantage of Next's built-in API functionality. With those values in place, let's make some updates to the boilerplate code in the pages > api folder of the project. There will be a file in this folder called hello.ts. Rename it to notication.ts and open it. Delete all of the existing code out of the file and replace it with this:

1// notification.ts
2import type { NextApiRequest, NextApiResponse } from "next";
3import { WebClient, LogLevel } from "@slack/web-api";
4
5const client = new WebClient(process.env.SLACK_TOKEN);
6
7type MessageData = {
8 channelName: string;
9 imageName: string;
10 imageUrl: string;
11};
12
13export default async function handler(
14 req: NextApiRequest,
15 res: NextApiResponse
16) {
17 const body = req.body;
18
19 await publishMessage({
20 ...body,
21 channelName: "zzz_test",
22 });
23
24 res.send("image uploaded successfully");
25 res.status(200);
26}
27
28async function publishMessage(messageData: MessageData) {
29 const channelId = await getChannelId(messageData.channelName);
30
31 try {
32 if (channelId) {
33 await client.chat.postMessage({
34 token: process.env.SLACK_TOKEN,
35 channel: channelId,
36 text: `The ${messageData.imageName} image upload is ready at ${messageData.imageUrl}`,
37 });
38 }
39 } catch (error) {
40 console.error(error);
41 }
42}
43
44async function getChannelId(name: string) {
45 try {
46 const result = await client.conversations.list({
47 token: process.env.SLACK_TOKEN,
48 });
49
50 if (result.channels) {
51 const channel = result.channels.find((channel) => {
52 return channel.name?.includes(name);
53 });
54
55 return channel?.id;
56 }
57 } catch (error) {
58 console.error(error);
59 }
60}

Let's step through this code piece by piece. We start with the imports we'll need from a couple of packages. Then we instantiate a new instance of the Slack client using our bot token. This client is how we'll execute commands to post to the channel we want via the Slack API. Then we move on to define the type for the MessageData we need to send the message to the Slack channel.

Next, we define the handler function. This function is required for all endpoints in using Next as the back-end. In this function, we get the body from the request, call a function that will publish a message to Slack with the info we have, and then send a response back to the requester.

The publishMessage function first finds the channelId based on the channel name we pass in. So the getChannelId uses the Slack client to find the channel ID based on the name by looping through all of the channels in your Slack workspace. When it finds a matching channel, it sends that ID back.

Then we take the channelId and use it in the Slack client to post a message to that specific channel. The message has the name of the image we uploaded and a Cloudinary link to view it. Everything is wrapped in try-catch statements so we should catch any errors that pop up along the way.

That finishes up the back-end work! Now let's move over to the front-end where users will upload those images to trigger the Slack message to be posted.

Build the front-end to upload images

The front-end will be pretty bare-bones in terms of the user interface. We'll have an image upload element and a button on the screen, but the majority of the code will handle the API request logic. We're going to work in the pages > index.tsx file so open that and delete the existing code and replace it with the following:

1import type { NextPage } from "next";
2import { useState } from "react";
3import styles from "../styles/Home.module.css";
4
5const Home: NextPage = () => {
6 const [uploadedImage, setUploadedImage] = useState<any>();
7
8 const uploadFn = async (e: any) => {
9 e.preventDefault();
10
11 const dataUrl = uploadedImage;
12
13 const uploadApi = `https://api.cloudinary.com/v1_1/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload`;
14
15 const formData = new FormData();
16 formData.append("file", dataUrl);
17 formData.append(
18 "upload_preset",
19 process.env.CLOUDINARY_UPLOAD_PRESET || ""
20 );
21
22 await fetch(uploadApi, {
23 method: "POST",
24 body: formData,
25 }).then(async (res) => {
26 const values = await res.json();
27
28 const data = {
29 name: values.original_filename,
30 url: values.url,
31 };
32
33 await fetch("/api/notification", {
34 method: "POST",
35 body: JSON.stringify(data),
36 });
37 });
38 };
39
40 return (
41 <div className={styles.container}>
42 <main className={styles.main}>
43 <input
44 type="file"
45 onChange={(e) => setUploadedImage(e.currentTarget.value)}
46 />
47 <button onClick={uploadFn}>Upload picture</button>
48 </main>
49 </div>
50 );
51};
52
53export default Home;

There's a bit going on here so let's walk through it. As usual, we start by importing the packages we'll need. We will be keeping some of the styles that were part of the boilerplate code to make things a little better. Then we jump right into the Home component. We start by declaring the uploadedImage state.

Trigger the Slack message with a successful upload

Then we get to the big part of this component. The uploadFn function is how we're going to upload an image to Cloudinary, get the image name and Cloudinary link, and make the Slack request that we built on the back-end.

First, we define the dataUrl which is where the image data will come from. Then we define the Cloudinary upload endpoint we'll be using with the credentials we got from the Cloudinary dashboard earlier. Next, we make a new FormData object that will be sent in the upload request.

After that, we wait for a response from the Cloudinary endpoint and take that data to use in our notification endpoint. This will send the Slack message to the channel we specified.

Finally, at the bottom, you see the two elements that are rendered on the page. There's a simple <input> element so users can upload files and a button they click when they're ready to send the image to Cloudinary.

We're done! Start the app locally with yarn dev and watch the messages go to your Slack channel.

Finished code

You can check out all of the code in the slack-notifs folder of this repo. You can also check out the app in this Code Sandbox.

Conclusion

Working with third-party APIs comes up in almost every app. Since Slack is a commonly used tool across the tech industry, having some practice working with it and the issues that arise will help sharpen your developer skills and push you to think about implementations from a few angles. Just always make sure to check the documentation!

Milecia

Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.