·4 min read

The Most Minimalist Next.js TODO app

Enes AkarEnes AkarCofounder @Upstash

In this article, we will build a Serverless Next.js based TODO application. We will try our best to make it minimalist. It will not have any database connection. It will not have any extra dependency other than Next.js. It will not have any buttons. Besides, minimalism is cool and clean, I love it because I am a lazy developer :)

Why do we avoid database connections?

Next.js is a modern framework which enables the front-end developers to develop full stack applications. Serverless functions have an important role in simplifying backend development for Next.js developers. As you probably know, serverless functions do not like database connections due to their stateless nature. See here and here as examples of problems of database connections inside serverless functions.

REST is an answer

REST allows client and server to communicate with no session information. This statelessness and its simple nature makes REST a perfect communication protocol for serverless environments. We will access Upstash Redis with REST.

The Project Stack

See the demo: https://nextjs-todo-zeta.vercel.app/

See the code: https://github.com/upstash/examples/tree/master/examples/nextjs-todo

The Project Setup

Create a Next.js app: npx create-next-app

Create an Upstash Redis® database in AWS-US-EAST-1 region and copy the REST URL and token.

The project will be a single page application with 3 API endpoints:

  • pages/api/list.js: Lists the TODO items.
  • pages/api/add.js: Adds a TODO item.
  • pages/api/remove.js: Removes a TODO item.

The Code

Add pages/api/list.js as below:

export default async (req, res) => {
  const token = "REPLACE_YOUR_TOKEN";
  const url = "https://REPLACE_YOUR_ENDPOINT/lrange/todo/0/100?_token=" + token;
 
  return fetch(url)
    .then((r) => r.json())
    .then((data) => {
      let result = JSON.stringify(data.result);
      return res.status(200).json(result);
    });
};
export default async (req, res) => {
  const token = "REPLACE_YOUR_TOKEN";
  const url = "https://REPLACE_YOUR_ENDPOINT/lrange/todo/0/100?_token=" + token;
 
  return fetch(url)
    .then((r) => r.json())
    .then((data) => {
      let result = JSON.stringify(data.result);
      return res.status(200).json(result);
    });
};

Add pages/api/add.js as below:

export default async (req, res) => {
  if (!req.query.todo) {
    return res.status(400).send("todo parameter required.");
  }
  let todo = encodeURI(req.query.todo);
 
  const token = "REPLACE_YOUR_TOKEN";
  const url =
    "https://REPLACE_YOUR_ENDPOINT/lpush/todo/" + todo + "?_token=" + token;
 
  return fetch(url)
    .then((r) => r.json())
    .then((data) => {
      let result = JSON.stringify(data.result);
      return res.status(200).json(result);
    });
};
export default async (req, res) => {
  if (!req.query.todo) {
    return res.status(400).send("todo parameter required.");
  }
  let todo = encodeURI(req.query.todo);
 
  const token = "REPLACE_YOUR_TOKEN";
  const url =
    "https://REPLACE_YOUR_ENDPOINT/lpush/todo/" + todo + "?_token=" + token;
 
  return fetch(url)
    .then((r) => r.json())
    .then((data) => {
      let result = JSON.stringify(data.result);
      return res.status(200).json(result);
    });
};

Add pages/api/remove.js as below:

export default async (req, res) => {
  if (!req.query.todo) {
    return res.status(400).send("todo parameter required.");
  }
  let todo = encodeURI(req.query.todo);
 
  const token = "REPLACE_YOUR_TOKEN";
  const url =
    "https://REPLACE_YOUR_ENDPOINT/lrem/todo/1/" + todo + "?_token=" + token;
 
  return fetch(url)
    .then((r) => r.json())
    .then((data) => {
      let result = JSON.stringify(data.result);
      return res.status(200).json(result);
    });
};
export default async (req, res) => {
  if (!req.query.todo) {
    return res.status(400).send("todo parameter required.");
  }
  let todo = encodeURI(req.query.todo);
 
  const token = "REPLACE_YOUR_TOKEN";
  const url =
    "https://REPLACE_YOUR_ENDPOINT/lrem/todo/1/" + todo + "?_token=" + token;
 
  return fetch(url)
    .then((r) => r.json())
    .then((data) => {
      let result = JSON.stringify(data.result);
      return res.status(200).json(result);
    });
};

Update pages/index.js as below:

import { useEffect, useState } from "react";
import Head from "next/head";
import Image from "next/image";
 
import styles from "../styles/Home.module.css";
 
export default function Home() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [todo, setTodo] = useState("");
  let changeHandler = (event) => {
    setTodo(event.target.value);
  };
 
  let addTodo = (event) => {
    setLoading(true);
    event.preventDefault();
    fetch("/api/add?todo=" + todo)
      .then((res) => res.json())
      .then((data) => {
        loadTodos();
      });
  };
 
  let removeTodo = (rtodo) => {
    setLoading(true);
    fetch("/api/remove?todo=" + rtodo)
      .then((res) => res.json())
      .then((data) => {
        loadTodos();
      });
  };
 
  let loadTodos = () => {
    console.log("load todos");
    fetch("/api/list")
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  };
 
  useEffect(() => {
    setLoading(true);
    loadTodos();
  }, []);
 
  if (!data) return "Loading...";
  return (
    <div className={styles.container}>
      <Head>
        <title>Next.js TODO APP</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
 
      <main className={styles.main}>
        <div className={styles.grid}>
          <h1 className={styles.title}>
            TODO App with{" "}
            <a href="https://blog.upstash.com/nextjs-todo">Next.js!</a>
            <br />
            <br />
          </h1>
          {loading ? (
            <a href="#" className={styles.card}>
              <img src="/loader.gif" />
            </a>
          ) : (
            <form className={styles.cardForm} onSubmit={addTodo}>
              <input
                className={styles.cardInput}
                type="text"
                name="todo"
                onChange={changeHandler}
                placeholder="Enter your exciting TODO item!"
              />
            </form>
          )}
 
          {data.map((item) => (
            <a
              href="#"
              onClick={() => removeTodo(item)}
              className={styles.card}
            >
              <p>{item}</p>
            </a>
          ))}
        </div>
      </main>
 
      <footer className={styles.footer}>
        <a
          href="https://blog.upstash.com/nextjs-todo"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{" "}
          <span className={styles.logo}>
            <Image src="/logo.png" alt="Upstash Logo" width={87} height={25} />
          </span>
        </a>
      </footer>
    </div>
  );
}
import { useEffect, useState } from "react";
import Head from "next/head";
import Image from "next/image";
 
import styles from "../styles/Home.module.css";
 
export default function Home() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [todo, setTodo] = useState("");
  let changeHandler = (event) => {
    setTodo(event.target.value);
  };
 
  let addTodo = (event) => {
    setLoading(true);
    event.preventDefault();
    fetch("/api/add?todo=" + todo)
      .then((res) => res.json())
      .then((data) => {
        loadTodos();
      });
  };
 
  let removeTodo = (rtodo) => {
    setLoading(true);
    fetch("/api/remove?todo=" + rtodo)
      .then((res) => res.json())
      .then((data) => {
        loadTodos();
      });
  };
 
  let loadTodos = () => {
    console.log("load todos");
    fetch("/api/list")
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  };
 
  useEffect(() => {
    setLoading(true);
    loadTodos();
  }, []);
 
  if (!data) return "Loading...";
  return (
    <div className={styles.container}>
      <Head>
        <title>Next.js TODO APP</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
 
      <main className={styles.main}>
        <div className={styles.grid}>
          <h1 className={styles.title}>
            TODO App with{" "}
            <a href="https://blog.upstash.com/nextjs-todo">Next.js!</a>
            <br />
            <br />
          </h1>
          {loading ? (
            <a href="#" className={styles.card}>
              <img src="/loader.gif" />
            </a>
          ) : (
            <form className={styles.cardForm} onSubmit={addTodo}>
              <input
                className={styles.cardInput}
                type="text"
                name="todo"
                onChange={changeHandler}
                placeholder="Enter your exciting TODO item!"
              />
            </form>
          )}
 
          {data.map((item) => (
            <a
              href="#"
              onClick={() => removeTodo(item)}
              className={styles.card}
            >
              <p>{item}</p>
            </a>
          ))}
        </div>
      </main>
 
      <footer className={styles.footer}>
        <a
          href="https://blog.upstash.com/nextjs-todo"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{" "}
          <span className={styles.logo}>
            <Image src="/logo.png" alt="Upstash Logo" width={87} height={25} />
          </span>
        </a>
      </footer>
    </div>
  );
}

As you see, it is a basic React application which uses hooks. We have 3 methods which interact with APIs: addTodo, removeTodo and loadTodos.

And finally update the styles/Home.module.css file as here.

Run and Deploy

Run your project locally with npm run dev. If everything looks good, you can deploy your project by running vercel in the project folder. Vercel will create serverless functions for your API functions. The default region for Vercel functions is US-EAST-1, that’s why we created our database in the same region.

NOTES

  • It is safer to keep the database token in a Vercel environment variable.
  • It is best for performance to keep the serverless function and Redis® database in the same region.
  • We could use Redis® clients instead of REST API. But as I mentioned before, the database connections can cause issues inside serverless functions. Also note that we did not see a major performance difference between Upstash REST API and native API.