Persistent To Dos with Next.js + Deta Base in 7 minutes
Tags: tutorial, database, react, javascript
Published: 2020-11-11 08:29:03

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](https://vercel.com/), 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](https://dev.to/deta/choosing-the-right-database-for-serverless-applications-2ijb), where persistent connections don't mesh well with asynchronous, ephemeral, functions; Vercel suggests  [connection pooling](https://vercel.com/docs/solutions/databases#connecting-to-your-database) 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](https://dev.to/deta/introducing-guide-a-gui-for-your-deta-base-2e40) can be used to update To Do state, feeding back into our Next.js app. 

This app uses the  [Create Next App](https://nextjs.org/docs/api-reference/create-next-app) starter, and the full source code is [here](https://github.com/xeust/base-nextjs).

Deploy instructions are [here](#final-things).


## Design

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

```json
{
	"content": "Wake Up On Time", // string
	"isCompleted": false // boolean
}
```

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](https://nextjs.org/docs/api-reference/create-next-app) starter).

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

```

## 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](https://github.com/xeust/base-nextjs/blob/master/pages/index.js#L84)):

```jsx
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>
	...
	)

}
```

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](https://github.com/xeust/base-nextjs/blob/master/pages/api/todos/index.js)):

```jsx
// 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);
}
```

In this handler, we access a `project key` that we get from [Deta](https://www.deta.sh/) and store in a [Vercel Environment Variable](https://vercel.com/docs/serverless-functions/introduction#environment-variables). 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](https://docs.deta.sh/docs/base/sdk), we can take the `content` from the api call, add an `isCompleted` field, and use the [put](https://docs.deta.sh/docs/base/sdk#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](https://github.com/xeust/base-nextjs/blob/master/pages/index.js#L89) and [106](https://github.com/xeust/base-nextjs/blob/master/pages/index.js#L106) of `index.js`).

This relies on us having a working [ToDo component](https://github.com/xeust/base-nextjs/blob/master/pages/index.js#L5), which we will assume correctly displays content and completion status for now.

```jsx
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>

	...

	)

}		
```

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

```jsx
// 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);
}
```

Here the `GET` request is handled in the function, using a [Deta Base's fetch](https://docs.deta.sh/docs/base/sdk#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):

```jsx
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)}
	    />
	...
	)
}

```

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

```jsx
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);
}
```

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:

```jsx
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)}
				/>	
	...
	)

}
```

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

```jsx
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);
}
```

In this handler, we pass use the [delete method](https://docs.deta.sh/docs/base/sdk#delete) 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](https://www.deta.sh/) 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](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/xeust/base-nextjs/)


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](https://dev-to-uploads.s3.amazonaws.com/i/9wv2xfx3d0jn213j8fzt.png)


![Screen_Shot_2020-11-10_at_18.02.16](https://dev-to-uploads.s3.amazonaws.com/i/bksc70z186quqrpighgg.png)


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.