loading...
Deta

Persistent To Dos with Next.js + Deta Base in 7 minutes

xeust profile image Max ・7 min read

Next.js adds a lot on top of React; with support for api routes with serverless functions out of the box, Next let's you do traditional 'server side' tasks, like making authenticated requests to a database. If you deploy on Vercel, the pages/api directory will auto deploy as functions.

As we spoke about previously, traditional databases are not a great fit in the serverless model, where persistent connections don't mesh well with asynchronous, ephemeral, functions; Vercel suggests connection pooling as one way to mitigate these issues. Using a pure serverless database—where database requests do not rely on a persistent database connection—is another way around this issue.

This tutorial will guide you through creating a To Do app using Next.js and Deta Base, with deployments on Vercel. This app will be fundamentally different from a client side state model where To Do state is only stored in a React Component. In this app, the serverless functions will talk to Deta Base which will store the To Do state. This will provide To Dos a persistence that extends beyond component unmount and, as will be seen, Deta Base's GUI can be used to update To Do state, feeding back into our Next.js app.

This app uses the Create Next App starter, and the full source code is here.

Deploy instructions are here.

Design

The fundamental unit of our application will be a To Do, which will exist as a JSON object:

{
    "content": "Wake Up On Time", // string
    "isCompleted": false // boolean
}
Enter fullscreen mode Exit fullscreen mode

These to dos will be stored in Deta Base and ultimately rendered by our Next.js app. To do so requires adding the deta dependency to your project using npm install deta or yarn add deta.

Additionally, our Next.js app needs to be able to generate and interact with this data. We can tie the four basic CRUD functions to two endpoints / serverless functions in Next.js

  • Create a new To Do: POST api/todos
  • Read all the To Dos: GET api/todos
  • Update a To Do (of id tid ): PUT api/todos/{tid}
  • Delete a To Do (of id tid ): DELETE api/todos/{tid}

The basic Next.js file structure for our application is as follows (modified from the Create Next App starter).

/pages
    index.js (our frontend logic)
    /api
            /todos
                index.js (function, will handle the GET & POST)
                [tid].js (function, will handle the PUT & DELETE)

Enter fullscreen mode Exit fullscreen mode

Creating a To Do

To create a To Do, let's create an api call that will call POST api/todos based on some newContent stored in a React State Hook (this is tied to an input element in line 84):

export default function Home() {

  const [newContent, setNewContent] = useState('');

  ...


  const createToDo = async () => {
    const resp = await fetch('api/todos', 
      {
        method: 'post', 
        body: JSON.stringify({content: newText})
      }
    );
    // await getToDos(); To Be Implemented
  }

    ...
    return (
    ...
            <input className={styles.inpt} onChange={e => setNewContent(e.target.value)}></input>
    ...
    )

}
Enter fullscreen mode Exit fullscreen mode

The function createToDo, when called, will pull the value of newContent from state in React and POST it to our endpoint, which we handle at pages/api/todos/index.js (link here):

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { Deta } from 'deta';

const deta = Deta(process.env.DETA_PROJECT_KEY);

const base = deta.Base('todos');

export default async (req, res) => {
  let { body, method } = req;
  let respBody = {};

  if (method === 'GET') {

    // To Be Implemented

  } else if (method === 'POST') {

    body = JSON.parse(body);
    body.isCompleted = false;
    respBody = await base.put(body);
    res.statusCode = 201;

  }

  res.json(respBody);
}
Enter fullscreen mode Exit fullscreen mode

In this handler, we access a project key that we get from Deta and store in a Vercel Environment Variable. This key allows us to talk to any Base in that Deta project, in this case a database we have called todos. Using the Deta SDK, we can take the content from the api call, add an isCompleted field, and use the put method to store our new to do in our database. A key will be automatically generated under which this item will be stored.

Reading To Dos

To read all our To Dos, let's create an api call that will call GET api/todos and store it in a React hook in the home component of pages/index.js .

Secondly, let's also use a React useEffect hook to call this function when our component mounts.

Third, let's create two lists from our to dos, that will give us the list of to dos by completion status, which we will display in different parts of our app (lines 89 and 106 of index.js).

This relies on us having a working ToDo component, which we will assume correctly displays content and completion status for now.

export default function Home() {

  const [newContent, setNewContent] = useState('');

  const [toDos, setToDos] = useState([]);

  const getToDos = async () => {
    const resp = await fetch('api/todos');
    const toDos = await resp.json();
    setToDos(toDos);
  }

    ...

  useEffect(() => {
    getToDos();
  }, [])

  const completed = toDos.filter(todo => todo.isCompleted);
  const notCompleted = toDos.filter(todo => !todo.isCompleted);

    ...

  return (

    ...

     <div className={styles.scrolly}>
        {notCompleted.map((todo, index) => 
          <ToDo 
            key={todo.key} 
            content={`${index + 1}. ${todo.content}`} 
            isCompleted={todo.isCompleted} 
            // onChange={() => updateToDo(todo)} To Be Implemented
            // onDelete={() => deleteToDo(todo.key)} To Be Implemented
          />
        )}
     </div>

    ...

     <div className={styles.scrolly}>
       {completed.map((todo, index) => 
         <ToDo 
           key={todo.key} 
           content={`${index + 1}. ${todo.content}`} 
           isCompleted={todo.isCompleted}
           // onChange={() => updateToDo(todo)} To Be Implemented
           // onDelete={() => deleteToDo(todo.key)} To Be Implemented
         />
       )}
    </div>

    ...

    )

}       
Enter fullscreen mode Exit fullscreen mode

The serverless function handler in pages/api/todos/index.js looks as follows:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { Deta } from 'deta';

const deta = Deta(process.env.DETA_PROJECT_KEY);

const base = deta.Base('todos');

export default async (req, res) => {
  let { body, method } = req;
  let respBody = {};

  if (method === 'GET') {

    const {value: items} = await base.fetch([]).next();
    respBody = items;
    res.statusCode = 200;

  }

...

  res.json(respBody);
}
Enter fullscreen mode Exit fullscreen mode

Here the GET request is handled in the function, using a Deta Base's fetch to return all items in a database called todos .

Updating a To Do

To update a To Do's completion status, we create a function updateToDo that will call PUT api/todos/{tid} based on our ToDo component triggering an onChange function (which is implemented by a checkbox being checked / unchecked):

export default function Home() {

    ...
    const updateToDo = async (todo) => {
            let newBody = { 
               ...todo,
               isCompleted: !todo.isCompleted
            };
            const resp = await fetch(`api/todos/${todo.key}`, 
               {
                   method: 'put', 
                   body: JSON.stringify(newBody)
               }
            );

            await getToDos();
        }
    ...
    return (
    ...

            <ToDo 
                key={todo.key} 
                content={`${index + 1}. ${todo.content}`} 
                isCompleted={todo.isCompleted} 
                onChange={() => updateToDo(todo)}
        />
    ...
    )
}

Enter fullscreen mode Exit fullscreen mode

The function will send a PUT to with the opposite pages/api/todos/[tid].js :

import { Deta } from 'deta';

const deta = Deta(process.env.DETA_PROJECT_KEY);

const base = deta.Base('todos');

export default async (req, res) => {

  let { body, method, query: { tid } } = req;
  let respBody = {};

  if (method === 'PUT') {

    body = JSON.parse(body);
    respBody = await base.put(body);
    res.statusCode = 200;

  } else if (method === 'DELETE') {

    // To Be Implemented

  }

  res.json(respBody);
}
Enter fullscreen mode Exit fullscreen mode

In this handler, we pass the unchanged body through our put method to store our updated to do in our database. Because the body contains the key this will correctly overwrite the old record.

Deleting a To Do

Finally, to delete a To Do, let's add the api call that will call DELETE api/todos/{tid} based on a button click:

export default function Home() {

  ...


  const deleteToDo = async (tid) => {
    const resp = fetch(`api/todos/${tid}`, {method: 'delete'});
    setTimeout(getToDos, 200);
  }

    ...
    return (
    ...
                <ToDo 
                  key={todo.key} 
                  content={`${index + 1}. ${todo.content}`} 
                  isCompleted={todo.isCompleted} 
                  onChange={() => updateToDo(todo)}
                  onDelete={() => deleteToDo(todo.key)}
                />  
    ...
    )

}
Enter fullscreen mode Exit fullscreen mode

The function deleteToDo, when called, will make a DELETE request to pages/api/todos/{tid} , whose handler looks as follows:

import { Deta } from 'deta';

const deta = Deta(process.env.DETA_PROJECT_KEY);

const base = deta.Base('todos');

export default async (req, res) => {

  let { body, method, query: { tid } } = req;
  let respBody = {};

  if (method === 'PUT') {

  ...

  } else if (method === 'DELETE') {

    respBody = await base.delete(tid);
    res.statusCode = 200;

  }

  res.json(respBody);
}
Enter fullscreen mode Exit fullscreen mode

In this handler, we pass use the delete method from the Deta SDK.

Final Things

All the logic is implemented at this point and you can deploy the resulting application yourself to Vercel.

You can also do it in a few clicks: just grab a Deta project key, click the button below, and set the project key as an environment variable-- DETA_PROJECT_KEY--during Vercel's flow.

Deploy with Vercel

We can't forget to mention that you can now view and manage your to dos from Deta Base's GUI, Guide. If you add or modify one of your To Dos from here, the changes will load in the Vercel app on page refresh.

Screen Shot 2020-11-10 at 17.59.11

Screen_Shot_2020-11-10_at_18.02.16

The last thing worth mentioning is that this app uses a standard vanilla React pattern for managing the application state to keep things simple. However, we can take advantage of some smart things Next enables (in tandem with libraries like useSWR) to improve performance. If you've deployed this app, you'll notice the delays on create, modification and deletion, as the serverless functions take around 300ms to respond. With some improvements, we can boost performance and create a feeling of an instant response on the client side. Stay tuned for round 2.

Discussion

pic
Editor guide