> ## Documentation Index
> Fetch the complete documentation index at: https://upstash.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Roadmap Voting App with Serverless Redis

> This is a single page application powered by upstash and next.js.

<Info>
  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](https://roadmap.upstash.com). See [the blog
  post](https://blog.upstash.com/roadmap-application) to learn about it. The
  below example allows users to request features anonymously.
</Info>

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](https://roadmap.upstash.com)

### Deploy Yourself

You can check the source code of the complete application
[here](https://github.com/upstash/serverless-examples/tree/master/roadmap-voting-app).
Thanks to [Upstash\&Vercel integration](https://vercel.com/integrations/upstash),
you can deploy the application yourself with zero cost/code by clicking below:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fupstash%2Fserverless-tutorials%2Ftree%2Fmaster%2Froadmap-voting-app\&env=LOGO\&envDescription=Enter%20URL%20for%20your%20project%2Fcompany%20logo\&envLink=https%3A%2F%2Fdocs.upstash.com%2Fdocs%2Ftutorials%2Froadmap_voting_app\&project-name=roadmap-voting\&repo-name=roadmap-voting\&demo-title=Roadmap%20Voting\&demo-description=Roadmap%20Voting%20Page%20for%20Your%20Project\&demo-url=https%3A%2F%2Froadmap.upstash.com\&integration-ids=oac_V3R1GIpkoJorr6fqyiwdhl17)

### 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](https://github.com/upstash/serverless-tutorials/blob/master/roadmap-voting-app/pages/index.js)

```javascript theme={"system"}
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`.

<Snippet file="redis/ioredisnote.mdx" />

```javascript theme={"system"}
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.

```javascript theme={"system"}
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.

```javascript theme={"system"}
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.

```javascript theme={"system"}
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](https://github.com/upstash/serverless-tutorials/blob/master/roadmap-voting-app/pages/index.css)
helps page to look good and
[utils.js](https://github.com/upstash/serverless-tutorials/blob/master/roadmap-voting-app/pages/api/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](https://console.upstash.com)
