We have developed an advanced version of Roadmap Voting App where users should
log in to request or vote for a new feature. See it live version in Upstash
Roadmap. See the blog
post to learn about it. The
below example allows users to request features anonymously.
In this tutorial we will write a single page application which uses Redis as
state store in a Next.js application.
The example is a basic roadmap voting application where users enter and vote for
feature requests. You can check the complete application in
Upstash Roadmap page
Deploy Yourself
You can check the source code of the complete application
here.
Thanks to Upstash&Vercel integration,
you can deploy the application yourself with zero cost/code by clicking below:
Create Next.js Project
We will use Next.js as web framework. So let’s create a next.js app and install
the redis client first.
npx create-next-app nextjs-with-redis
npm install ioredis
index.js
Our application will be a single page. We will list the features with their
order of votes. There will be 3 actions available for the page user:
- The user will suggest a new feature.
- The user will vote up an existing feature.
- The user will enter their email to be notified of a release of any feature.
The below are the parts that handles all those. If you want to check the full
page see
here
import Head from 'next/head'
import { ToastContainer, toast } from 'react-toastify';
import * as React from "react";
class Home extends React.Component {
...
refreshData() {
fetch("api/list")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.body
});
this.inputNewFeature.current.value = "";
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
vote(event, title) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({"title": title})
};
console.log(requestOptions);
fetch('api/vote', requestOptions)
.then(response => response.json()).then(data => {
console.log(data)
if(data.error) {
toast.error(data.error, {hideProgressBar: true, autoClose: 3000});
} else {
this.refreshData()
}
})
}
handleNewFeature(event) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({"title": this.inputNewFeature.current.value})
};
fetch('api/create', requestOptions)
.then(response => response.json()).then(data => {
if(data.error) {
toast.error(data.error, {hideProgressBar: true, autoClose: 5000});
} else {
toast.info("Your feature has been added to the list.", {hideProgressBar: true, autoClose: 3000});
this.refreshData()
}
});
event.preventDefault();
}
handleNewEmail(event) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({"email": this.inputEmail.current.value})
};
console.log(requestOptions);
fetch('api/addemail', requestOptions)
.then(response => response.json()).then(data => {
if(data.error) {
toast.error(data.error, {hideProgressBar: true, autoClose: 3000});
} else {
toast.info("Your email has been added to the list.", {hideProgressBar: true, autoClose: 3000});
this.refreshData()
}
});
event.preventDefault();
}
}
export default Home;
APIs
With Next.js, you can write server-side APIs within your project. We will have 4
apis:
- list features
- vote a feature
- add a new feature
- add email
Now let’s examine these API implementations:
list.js
The list API connects to the Redis and fetches feature requests ordered by their
scores (votes) from the Sorted Set roadmap
.
This example uses ioredis, you can copy the connection string from the
Node tab in the console.
import { fixUrl } from "./utils";
import Redis from "ioredis";
module.exports = async (req, res) => {
let redis = new Redis(fixUrl(process.env.REDIS_URL));
let n = await redis.zrevrange("roadmap", 0, 100, "WITHSCORES");
let result = [];
for (let i = 0; i < n.length - 1; i += 2) {
let item = {};
item["title"] = n[i];
item["score"] = n[i + 1];
result.push(item);
}
redis.quit();
res.json({
body: result,
});
};
create.js
This API connects to the Redis server and add a new element to the sorted set
(roadmap) . We use “NX” flag together with ZADD, so a user will not be able to
overwrite an existing feature request with the same title.
import Redis from "ioredis";
import { fixUrl } from "./utils";
module.exports = async (req, res) => {
let redis = new Redis(fixUrl(process.env.REDIS_URL));
const body = req.body;
const title = body["title"];
if (!title) {
redis.quit();
res.json({
error: "Feature can not be empty",
});
} else if (title.length < 70) {
await redis.zadd("roadmap", "NX", 1, title);
redis.quit();
res.json({
body: "success",
});
} else {
redis.quit();
res.json({
error: "Max 70 characters please.",
});
}
};
vote.js
This API updates (increments) the score of the selected feature request. It also
keeps the IP addresses of the user to prevent multiple votes on the same feature
request.
import Redis from "ioredis";
import { fixUrl } from "./utils";
module.exports = async (req, res) => {
let redis = new Redis(fixUrl(process.env.REDIS_URL));
const body = req.body;
const title = body["title"];
let ip = req.headers["x-forwarded-for"] || req.headers["Remote_Addr"] || "NA";
let c = ip === "NA" ? 1 : await redis.sadd("s:" + title, ip);
if (c === 0) {
redis.quit();
res.json({
error: "You can not vote an item multiple times",
});
} else {
let v = await redis.zincrby("roadmap", 1, title);
redis.quit();
res.json({
body: v,
});
}
};
addemail.js
This API simply adds the user’s email to the Redis Set. As the Set already
ensures the uniqueness, we only need to check if the input is a valid email.
import Redis from "ioredis";
import { fixUrl } from "./utils";
module.exports = async (req, res) => {
let redis = new Redis(fixUrl(process.env.REDIS_URL));
const body = req.body;
const email = body["email"];
redis.on("error", function (err) {
throw err;
});
if (email && validateEmail(email)) {
await redis.sadd("emails", email);
redis.quit();
res.json({
body: "success",
});
} else {
redis.quit();
res.json({
error: "Invalid email",
});
}
};
function validateEmail(email) {
const re =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
css and utils
index.css
helps page to look good and
utils.js
fixes common mistakes on Redis URL.
Notes:
- If you deploy this application with Vercel; Vercel runs AWS Lambda functions
to back the API implementations. For best performance choose the the same
region for both Vercel functions and Upstash cluster.
- You can access your database details via
Upstash Console