Brenelz Web Solutions

Brenley Dueck

Home|Posts|Contact

Building a Todo List with Supabase and NextJS

April 10, 2021

So recently I've been feeling super productive using nextjs for spinning up simple projects. The part that always trips me up in projects is when I need to reach for a database. There are so many options like Hasura, Fauna, Mongo, etc. I've played with most of these but recently I've been exploring supabase. It seems so easy so I've been wanting to try it out on a project.

I set out to build out the simplest todo list I could (because I didn't have better ideas). I then figured I would turn it into a blog post to potentially help others.

What we will be building

We will build a simple todo list that you can follow along with. It is mainly just a proof of concept for doing CRUD actions with supabase and nextjs.

We will have a list of todo items, an ability to add new ones, complete existing ones and lastly delete them.

Here is an example of what the finished product looks like:

I assume a basic knowledge of React but I will try to explain the concepts as clearly as I can.

Getting started

In order to get the most out of this tutorial I would suggest you to clone down this repo.

git clone git@github.com:brenelz/supabase-todo-app.git
cd supabase-todo-app
git checkout start

There is a main branch which has all the code completed and a start branch where I have a blank canvas complete with some jsx and css so we don't have to worry about that.

As an alternative you can start from a completely blank canvas if you want using a command like the following npx create-next-app supabase-todo-app.

Tour of the start branch

package.json

The standard dependencies and scripts for a new nextjs project (next, react, react-dom)

.env.example

Contains sample environment keys that we will need to get from supabase later on. At this moment you can duplicate this file into .env.local.

cp .env.example .env.local

Next automatically loads this file in and puts the variables in process.env for you. Another thing to note is that for the env variables to be available on the frontend they need to start with NEXT_PUBLIC_

styles/globals.css

Just some super basic styles to make things look a tiny bit nicer.

pages/index.js

This is where you'll primarily be coding. I have stubbed out a few methods and written some basic jsx for you.

Setting up Supabase

Now first of all what is supabase you may ask. Like it says on the website it an open source firebase alternative. One thing I like better is it uses a postgres database instead of a nosql one which I personally am more used to.

It also has a super simple client js library that makes interacting with your data easy.

Create an account / sign in

Now you will want to go to supabase.io and sign in or create an account.

Create a new project and then go to settings, and then the api tab. Here you will want to copy your url and put it in your .env.local file for NEXT_PUBLIC_SUPABASE_URL. Also copy your anon public api key to NEXT_PUBLIC_SUPABASE_ANON_KEY

Setting up db tables


Head on over to the table editor and create a new table. Call it todos in this case and add the following columns:

• id (primary key)
• title (text)
• completed (bool)

Then feel free to add some todo items that we can display in our app. See how easy and intuitive this interface is? I love it!

Now Let's get coding

Reading our todo items

Let's install our dependencies, add the supabase client js library, as well as start the dev server using the following commands:

yarn install
yarn add @supabase/supabase-js
yarn dev

Then in a utils directory add a file called initSupabase.js with the following contents.

import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);

export default supabase;

Then head on over to pages/index.js

Here you notice there is an allTodos variable setup for you. We will want to pull this data in from supabase. Now here is the beauty of nextjs you can simply export a getServerSideProps function and do server side stuff (like pull from supabase). Let's add the import to the top of the file and the rest of the code at the bottom of the file:

import supabase from '../utils/initSupabase';

export async function getServerSideProps() {
  const { data } = await supabase.from("todos").select("id, title, completed");

  return {
    props: {
      todos: data,
    },
  };
}

The one thing to note is whatever you pass to the props object gets passed to your component. You can see how I am receiving it towards the top of the file:

export default function Home({ todos }) {

Just below this line one other change to make is to have allTodos point this new todos variable.

const allTodos = todos;

Now if you go back to your browser at localhost:3000 you should see the todo items you entered from supabase. It's that simple!

Next on the docket is building out the rest of the CRUD actions.

Adding todo items

First thing we need to do is store the state of whatever we've entered in the text area. We can do this with a simple useState.

Replace const newTodo = ""; with const [newTodo, setNewTodo] = useState("");

Reminder I have already imported useState for you.

Now we need to do something with this value that we have. Let's fill out addTodo presuming we have an api already built (we will get to this in a bit).

 const addTodo = async () => {
    const result = await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ newTodo }),
    });
    const data = await result.json();

    setNewTodo("");
  };

If you now hit the add button it should make the fetch call but get a 404. This is expected since we haven't setup our api routes yet. Let's do that.

Setup API Routes

Api routes in nextjs are as simple as creating a file in pages/api directory. In our case we will create a funky looking file. It will be located at pages/api/todos/[[...id]].js

This basically is a catch all route that matches any type of route like:

http://localhost:3000/api/todos
http://localhost:3000/api/todos/1

Now for dealing with POST, PUT, DELETE requests in a simple manner I like using a library called next-connect. Stop your dev server and run:

yarn add next-connect and then yarn dev again.

Now in your api route file you created above add the following:

import nc from "next-connect";
import supabase from "../../../utils/initSupabase";

const handler = nc({
  onError: (error, req, res, next) => {
    console.log(error);
    res.status(500).end();
  },
});

handler.post(async (req, res) => {
  const { newTodo } = JSON.parse(req.body);

  const result = await supabase.from("todos").insert([{ title: newTodo }]);

  const todo = result.data[0];
  res.send({ todo });
});

export default handler;

We are using next-connect to allow us to respond to a POST request. You will also notice we are inserting our newTodo param we passed in the body to supabase and then returning that updated record back.

If you go back to your app and try and add an item what happens? It appears like nothing but if you refresh the page they show up. This is because the POST request is successful but we aren't yet doing anything with the data returned from it.

Head back over to pages/index.js. We need to setup another useState hook that defaults to the todos passed from the getServerSideProps function but one that we can update with data when it comes back from our api.

Replace const allTodos = todos; with const [allTodos, setTodos] = useState(todos || []);

Then we can modify the addTodo funtion using setTodos and the spread operator.

const addTodo = async () => {
    const result = await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ newTodo }),
    });
    const data = await result.json();

    setNewTodo("");

    // this is the new line we need to add
    setTodos((state) => [...state, data.todo]);
  };

Now when you go back and add a todo it will show up as expected.

Building out the rest of our api

Back in api/pages/todos/[[...id]].js add the following code to the bottom of the file above the export default handler

handler.put(async (req, res) => {
  const { completed } = JSON.parse(req.body);

  const result = await supabase
    .from("todos")
    .update({ completed })
    .match({ id: req.query.id });

  const todo = result.data[0];

  res.send({ todo });
});

handler.delete(async (req, res) => {
  const result = await supabase
    .from("todos")
    .delete()
    .match({ id: req.query.id });

  const todo = result.data[0];

  res.send({ todo });
});

These api routes don't really show anything to new outside of how to delete and update using the supabase client library. One thing to note is the use of handler.put and handler.delete. Also you can pull the query parameter off the request by using req.query.id.

Finishing off the functionality

Now back in pages/index.js fill out the missing functions with the below. They are not too different from adding a todo.

const completeTodo = async (id) => {
    const result = await fetch("/api/todos/" + id, {
      method: "PUT",
      body: JSON.stringify({ completed: true }),
    });
    const data = await result.json();

    setTodos((state) =>
      state.map((todo) => (todo.id !== data.todo.id ? todo : data.todo))
    );
  };

 const deleteTodo = async (id) => {
    const result = await fetch("/api/todos/" + id, {
      method: "DELETE",
    });
    const data = await result.json();

    setTodos((state) => state.filter((todo) => todo.id !== data.todo.id));
  };

Now when you go back to your app you should be able to complete todos which show as a strikethrough and you can also delete them. Remember these changes are all persisting to the backend in supabase which is super cool!

The one thing to note here is how we are calling setTodos. For updating the todos we map over them and match up by id to determine if we should replace it or not. For deleting we just filter out the one we just deleted.

Conclusion

I know this has been a bit of a long post... probably one of the longer ones I have ever done. I hope you've picked up some neat tricks. I feel like nextjs and supabase make a great stack for prototyping projects in a quick manner. Obviously at larger scales this might not work as great for you.

Other features that supabase has is auth which I also really want to play with. It seems quite a bit easier than alternatives like Auth0 and even NextAuth.

Another thing they just released is supabase storage which I haven't looked at much.

Hope you enjoyed this, and remember you can view the finished repo and even deploy to netlify if you like.

Thoughts? Hit me up on twitter @brenelz