TL;DR
In this guide, we’ll make a simple shoping list webapp with Node and Express. Then, we’ll be using Novu as an open source notification system to send email reminders about our groceries on the day we need to get them.
Getting Some Groceries
I’ll admit I’m not the best when it comes to keeping track of things. Half the time, when I go to get groceries, I end up missing a chunk of stuff that totally slipped my mind. While I could easily just write things down, I’m also a dev and like to make things difficult for myself. So let’s throw something together!
In this article, we’ll be going over a simple shopping list that we’ll create with Node.js, as well as some pointers on how we can use the Novu platform to send emails from our API.
Setting Up Our Project
If you’d like to see the full project Github, you can check it out here.
Naturally, we’ll need to start our project somehow. The first step we need to do is to create a project folder and then add in a basic webpage.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./styles.css">
<link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.0/css/line.css">
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.0/dist/index.umd.min.js"></script>
<title>Grocery Notification</title>
</head>
<body>
<div class="container">
<div class="input">
<input name="Enter new item" id="grocery-input" placeholder="Enter new grocery item"></inp>
<i class="uil uil-notes notes-icon"></i>
</div>
<div class="datepicker-container">
<input id="datepicker" placeholder="Schedule Grocery Date" type="text"/>
</div>
<h1 class="title">Grocery Items</h1>
<ul class="grocery-list">
<li class="grocery-list-item" >
<span class="grocery-item">Eggs</span>
<i class="uil uil-trash delete-icon"></i>
</li>
</ul>
<button class="submit" type="button">Schedule</button>
</div>
<div class="error notification"></div>
<div class="success notification"></div>
<script src="./script.js"></script>
</body>
</html>
Our markup is pretty simple actually. We have our input where we specify the grocery item we need to add and the list of items. Lastly, we have an input date where we specify the date we want to be reminded.
We’re going to use a third-party date picker library called easepick. It’s a pretty simple library to drop into our app, and we’ll use it alongside icons from Unicons.
We’ll use some styling for our site too, which you can find here:
styles.css
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;1,300&family=Roboto&display=swap');
* {
font-family: 'Roboto', sans-serif;
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #eee;
height: 100vh;
padding: 0;
}
.container {
position: relative;
max-width: 500px;
width: 100%;
background-color: #fff;
box-shadow: 0 3px 5px rgba(0,0,0,0.1);
padding: 30px;
margin: 85px auto;
border-radius: 6px;
}
.container .input {
position: relative;
height: 70px;
width: 100%
}
.container .datepicker-container {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
.container input#datepicker {
padding: 8px;
outline: none;
border-radius: 6px;
border: 1px solid #cecece;
}
input#grocery-input {
height: 100%;
width: 100%;
outline: none;
border-radius: 6px;
padding: 25px 18px 18px 18px;
font-size: 16px;
font-weight: 400;
resize: none;
}
.notes-icon {
position: absolute;
top: 50%;
font-size: 15px;
right: 20px;
transform: translateY(-50%);
font-size: 18px;
color: #828282;
}
.title {
text-align: center;
margin-top: 25px;
margin-bottom: 0px;
}
.grocery-list {
margin-top: 30px;
}
.grocery-list .grocery-list-item {
list-style: none;
display: flex;
align-items: center;
width: 100%;
background-color: #eee;
padding: 15px;
border-radius: 6px;
position: relative;
margin-top: 15px;
}
.grocery-list .grocery-item {
margin-left: 15px;
}
.grocery-list .delete-icon {
position: absolute;
right: 15px;
cursor: pointer;
}
button.submit {
margin-top: 40px;
padding: 12px;
border-radius: 6px;
outline: none;
border: none;
width: 100%;
background-color: #0081C9;
color: white;
cursor: pointer;
}
.notification {
position: absolute;
top: 5%;
left: 50%;
transform: translate(-50%,-50%);
width: 250px;
height: auto;
padding: 5px;
margin-top: 5px;
border-radius: 6px;
color: white;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
}
.error {
background-color: #FF5733;
}
.success {
background-color: #0BDA51;
}
.notification.show {
opacity: 1;
-webkit-animation: fadein 0.5s, fadeout .5s 1.5s;
animation: fadein 0.5s, fadeout .5s 1.5s;
}
@keyframes fadein {
from {top: 0; opacity: 0;}
to {top: 5%; opacity: 1; }
}
@keyframes fadeout {
from {top: 5%; opacity: 1;}
to {top: 0; opacity: 0;}
}
Now that we have the looks, we need the functionality. So let’s add some JavaScript too:
script.js
window.addEventListener('DOMContentLoaded', () => {
const ulElement = document.querySelector('.grocery-list');
const submitElement = document.querySelector('.submit');
const inputElement = document.querySelector('#grocery-input');
const errorElement = document.querySelector('.error');
const successElement = document.querySelector('.success');
let dateSelected = null;
const showNotificationMessage = (element, errorMessage) => {
if (element.classList.contains('show')) {
return;
}
element.textContent = errorMessage;
element.classList.add('show');
setTimeout(() => {
element.classList.remove('show');
}, 2000)
}
const datePicker = new easepick.create({
element: '#datepicker',
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.0/dist/index.css"
],
zIndex: 10,
setup(picker) {
picker.on('select', (e) => {
dateSelected = e.detail.date;
})
}
});
ulElement.addEventListener('click', (e) => {
if (e.target.tagName === 'I') {
ulElement.removeChild(e.target.closest('li'));
}
})
inputElement.addEventListener('keyup', (e) => {
const value = e.target.value;
if (e.keyCode === 13 && value.trim()) {
const li = document.createElement('li');
li.classList.add('grocery-list-item');
const span = document.createElement('span');
span.classList.add('grocery-item');
span.textContent = value;
const icon = document.createElement('i');
icon.classList.add('uil', 'uil-trash', 'delete-icon');
li.appendChild(span);
li.appendChild(icon);
ulElement.appendChild(li);
inputElement.value = '';
}
});
submitElement.addEventListener('click', (e) => {
const groceryItems = [...document.querySelectorAll('span.grocery-item')].map(element => ({
item: element.textContent
}));
if (!dateSelected) {
return showNotificationMessage(errorElement, 'Please select the grocery date.');
}
const date2DaysBefore = new Date(dateSelected.setDate(dateSelected.getDate() - 1));
if (new Date() > date2DaysBefore) {
return showNotificationMessage(errorElement, 'Please select a date two days or more after this day.');
}
if (!groceryItems.length) {
return showNotificationMessage(errorElement, 'Please add grocery items.');
}
fetch('http://localhost:3000/grocery-schedule', {
method: 'POST',
body: JSON.stringify({
scheduledGroceryDate: dateSelected.toISOString(),
groceryItems
}),
headers: {
'content-type': 'application/json'
},
mode: 'cors'
})
.then(resp => resp.json())
.then((resp) => {
while (ulElement.lastChild) {
ulElement.removeChild(ulElement.lastChild);
}
showNotificationMessage(successElement, resp.message);
})
.catch(e => console.log(e))
})
})
In our script.js
file, we:
- Initialize our easepick instance, and in the
setup
method, we are listening to theselect
event so we can get the date value. - For
ulElement
we’re listening for theclick
event, and in the callback, we’re checking for theI
element because that is the delete button for our unordered list. - In
inputElement
we’re listening forkeyup
event so that we can add a new item in the unordered list element. - And lastly,
submitElement
is listening for theclick
event for this element and will send the request to our API will trigger scheduling.
We will be triggering the email the day before the scheduled date to remind us earlier.
This is what the web app looks like in the front end with one grocery item.
Creating Our API
Now we’ll be needing to create our backend with Node and Express. Specifically, we need Node version 18.12.0. There’s a command line utility that we can use to switch to the version we need.
mkdir api && cd api && npm init -y && npm install express cors @novu/node
What we did here is that we created the API folder and after that, initialize our node project with the npm init -y
command using the default configuration. After that, we installed the libraries that we will be using. We’re not saving our data in the database, but we’ll get to that later. We also installed the Novu @novu/node
package so that we can notify from our API.
package.json
{
"name": "grocery-notify-app",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@novu/node": "^0.11.0",
"cors": "^2.8.5",
"express": "^4.18.2"
},
"type": "module"
}
config.js
const API_KEY = 'API_KEY'; // Novu Dashboard -> Settings -> Api Keys Tab
const SUBSCRIBER_ID = 'SUBSCRIBER_ID'; // subscriber id created by the sdk or from the workflow
const EMAIL = 'TO_EMAIL'; // your EMAIL to receive the notification
const PORT = 3000;
export {
API_KEY,
EMAIL,
SUBSCRIBER_ID,
PORT
}
We will be getting API_KEY
and the SUBSCRIBER_ID
from the Novu Dashboard later.
app.js
import express from "express";
import cors from "cors";
import { API_KEY, SUBSCRIBER_ID, EMAIL, PORT } from "./config.js";
import { Novu } from "@novu/node";
const novu = new Novu(API_KEY);
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.post('/grocery-schedule', async (req, res) => {
const { scheduledGroceryDate, groceryItems } = req.body;
try {
res.status(200).send({ message: "Grocery Reminder Scheduled." })
} catch (e) {
console.log(e);
res.status(500).send({
message: 'Something went wrong, when scheduling the grocery reminder.'
})
}
});
app.listen(PORT, () => {
console.log(`server listening at port ${PORT}`);
})
In our app.js
we have one endpoint: /grocery-schedule
. This is where we will handle the notification that we receive in our email. Using Novu is really easy, getting the Novu
constructor from the module and creating a new Novu instance. Grab your API_KEY
from your Novu account dashboard.
What is Novu?
Basically, Novu is a platform for implementing, configuring and managing notifications in our application. It can manage:
- SMS
- Chat
- Push Notifications
All on one platform! Novu makes it easier when building real-time applications, all we need to do is to configure and trigger the notification in our code. You can read more about Novu here. You can easily create an account with Novu with your GitHub account, which is probably the fastest for this tutorial.
When you first see the Novu dashboard, you will be seeing the dashboard.
As I said earlier, we can get the API key here in the dashboard, specifically in Settings → Api Keys.
After that, we need to make a notification template that we will trigger in our API. Click **Notifications* and click the New button on the far right.
Put the necessary details about the notification template like the Notification Name, Notification Identifier, and Notification Description. Before we create the workflow, let’s take a look at the Integrations Store tab.
As I said earlier, Novu has a list of notification integration options that you can work with like email, sms, chat and push notifications. This is where you integrate the Notification Template that we made earlier into a specific provider. We’ll use Mailjet as the email provider to send the grocery reminder to a specific email.
First, your email must have a valid domain to make this work. I’ll be using my work email for this.
You can get your Mailjet API key and secret key in your Account Settings → Rest API → API Key Management (Primary and Sub-account).
Copy the API key and secret key and go to Novu Dashboard → Integrations* Store → Mailjet Provider and paste it. Make sure to include the email that you used for Mailjet.
After this, we need to edit our workflow editor for our notification. In your Novu dashboard go to Notifications and click the notification that you created earlier. For me it’s grocery-notification
.
Once we’ve done that, click the Workflow Editor
tab and in that in the editor click the circle with the plus sign below the Trigger component and select Delay
on the right side.
There are two types of Delay
, Regular and Scheduled. For this, we’ll use the Scheduled type. We also need to specify the name of the field that Novu will be using for email. In my example, I will be using the sendAt
field.
And below the Delay
component click the circle with the plus sign again and select Email
.
Our workflow should look like this:
Lastly, we also need to configure our email template. Click the Edit Template button on the right below Email Properties. After the redirection to the Edit Email Template UI, the email you’re seeing here is the email that you used in the Mailjet configuration earlier. Update the email subject to Grocery Reminder and click the Custom Code and copy this Handlebars code below:
<div>
<h1>Hi, It's time for you to buy your grocery items.</h1>
<h2>{{dateFormat date 'MM/dd/yyyy'}}</h2>
{{#each groceryItems}}
<li>{{item}}</li>
{{/each}}
</div>
This code is pretty simple. In the h2 we’re formatting the date
value to Month/Day/Year format, and in the part where we use the #each
we’re just iterating in our groceryItems
array. We’re also using the item
property that we get from each item iteration in the list item element, and we need to specify the /each
after the iteration.
After that, click the Update
button on the top right side.
Finishing Our API
With all of that out of the way, it’s time to complete the API of our app. Update the config.js
file with the API key you got earlier from the Novu Dashboard, the SUBSCRIBER_ID, and email from Notifications in the dashboard. The EMAIL is optional here to use since the SUBSCRIBER_ID will use that same value, but for example purposes, we will provide it.
And our updated config.js
file:
const API_KEY = 'YOUR_API_KEY_FROM_THE_SETTINGS_TAB'; // Novu Dashboard -> Settings -> Api Keys Tab
const SUBSCRIBER_ID = '63dd93575fd0df47313ee933';
const EMAIL = 'mac21macky@gmail.com';
const PORT = 3000;
export {
API_KEY,
EMAIL,
SUBSCRIBER_ID,
PORT
}
Lastly, we need to update our app.js
file to trigger Novu for scheduling our notification:
import express from "express";
import cors from "cors";
import { API_KEY, SUBSCRIBER_ID, EMAIL, PORT } from "./config.js";
import { Novu } from "@novu/node";
const novu = new Novu(API_KEY);
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.post('/grocery-schedule', async (req, res) => {
const { scheduledGroceryDate, groceryItems } = req.body;
try {
// sendAt - 9 am on the `scheduledGroceryDate`
const sendAt = new Date(new Date(scheduledGroceryDate).setHours(9, 0, 0, 0)).toISOString();
await novu.trigger('grocery-notification', {
to: {
subscriberId: SUBSCRIBER_ID,
email: EMAIL
},
payload: {
sendAt,
date: new Date(scheduledGroceryDate).toISOString(),
groceryItems
}
});
res.status(200).send({ message: "Grocery Reminder Scheduled." })
} catch (e) {
console.log(e);
res.status(500).send({
message: 'Something went wrong, when scheduling the grocery reminder.'
})
}
});
app.listen(PORT, () => {
console.log(`server listening at port ${PORT}`);
});
Basically, what we’re doing here is creating the scheduled date value sendAt
. This will have the value of 9 am of scheduledGroceryDate
. For example, if we provide the value of March 12, 2023, then the sendAt
value will be March 12, 2023 9:00 am. The trigger
method from the novu
instance from the name itself will trigger our notification. It accepts the Trigger ID as the first parameter, which you can get from Novu.
In the to
object we provide the SUBSCRIBER_ID and EMAIL from our config.js
and in the payload
object we provide sendAt
, date
and groceryItems
. Remember that sendAt
and date
fields must be in ISO string format.
Okay! Let’s test our application. I’m gonna add 5 new items to our grocery list: Milk, Bread, Water, Butter, and Grapes.
Press the Schedule button, and if went successfully, it will pop up a message that tells us it’s been scheduled.
Going back to Novu, in the Activity Feed, you can see if that particular reminder is scheduled.
And when you click the first one you can know the details of that notification.
The execution for this notification is delayed because we specified it as a Delay
type earlier and you can also see the time when this notification will be executed.
This is a sample email that you will get if it all goes as planned.
If you want to expand this project, you can add a database to our project for saving grocery items. Then, show the list of grocery dates that are scheduled on another page in our web app.
Closing the Shopping List
So far, we’ve created an app that uses some simple Node.js and Express to make a simple grocery list. Then, we use the power of Novu to schedule notifications and send emails to remind ourselves. On the surface, it’s pretty simple, but a lot of room for added functionality. You can set up user accounts and share it with your family, or maybe add SMS notifications too!
Novu is a great open source notification system that you can use for notifications to integrate with your applications. It’s free to get started, and a very powerful tool in the right hands.
If you’d like to see the full project Github, you can check it out here.
Top comments (6)
Nice Article, but be Careful storing sensitive data like api keys in javascript files, it's better to store them in .env file then throw this .env file in the .gitignore
just adding, by default, .env files already isn't added to commit or stage or anything else on git. So it's not needed.
I see a bug: mac21macky@gmail.com :)
Thanks for sharing! I don't know Novu. I will do the test
Can I write for novu?
I don't have know Novu, will do the test! thanks for sharing!