Persistent To Dos with Next.js + Deta Base in 7 minutes
Max
Posted on November 11, 2020
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
}
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)
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>
...
)
}
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);
}
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>
...
)
}
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);
}
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)}
/>
...
)
}
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);
}
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)}
/>
...
)
}
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);
}
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.
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.
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.
Posted on November 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.