Creating a Simple CRUD Todo List with Express.js

Introduction

Recently learning backend development, Express.js is the closest framework to frontend, ideal for full-stack engineers to quickly engage with both fields.

express-fastify-koa-restify-trend
Express.js Long-term Download Trends

This implementation does not involve any database-related parts, focusing on using Express.js to create a simple in-memory backend server to familiarize with CRUDd APIs.

Building a Minimal Working Express.js

JavaScript backend runs on Node.js🔗, and pick your favorite code editor, create a new folder, initialize an NPM project, and install Express.js.

Terminal window
# npm initialization (-y uses default values without further configuration)
$ npm init -y
# Install Express
$ npm install express

Next, just create a new app.js, write some Express.js related code, and run it with Node.js (node app.js). Below is the minimal working Express.js.

  • req is the conventional abbreviation for request
  • res is the conventional abbreviation for response
import express from 'express';
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});

We expose Express on port 3000, so just open a browser to the local address http://localhost:3000/ to see the Hello World example. Express has many methods to call; refer to the official documentation to learn more, such as: app.get()🔗, res.send()🔗.

Building CRUD APIs

1. Planning

The API of Express.js is very intuitive. Corresponding to HTTP request methods🔗, you can find methods with the same name in the official documentation. To build a todo list, we will need:

  1. todos data initialized when Express starts
  2. APIs for external interaction with the data
  3. Logic for data CRUD operations
let todos = [
// {
// id: 1, (unique identifier)
// title: 'Init Todo',
// isCompleted: false,
// },
];
// Get Todos
app.get();
// Add Todos
app.post();
// Modify Todos
app.put();
// Delete Todos
app.delete();

One thing to note is that Express does not parse res.body as JSON by default. We can use Middleware🔗 to handle all res.body using express.json🔗.

app.use(express.json());

2. Implementation

The remaining task is to write the internal logic of the server and interact with the data. I prefer to calculate a new todos independently and then reassign the content back, while paying attention to some exceptions, such as returning an error to the client when editing a non-existent ID.

// Get All Todos
app.get("/api/todos", (req, res) => {
res.json(todos)
})
// Create Todo
app.post("/api/todos", (req, res) => {
const { title } = req.body;
if (!title || title.trim() === "") {
return res.status(400).json({ error: "Title is required and cannot be empty." });
}
const newTodo = {
id: Date.now().toString(),
title,
isCompleted: false
}
todos = [...todos, newTodo]
res.status(201).json(newTodo)
})
// Edit {id} Todo
app.put("/api/todos/:id", (req, res) => {
const targetId = req.params.id
const targetIndex = todos.findIndex(todo => todo.id === targetId);
const isTargetTodoExist = targetIndex !== -1
if (!isTargetTodoExist) {
return res.status(404).json({ error: "Todo not found" });
}
const updatedTodo = {
...todos[targetIndex],
title: req.body.title ?? todos[targetIndex].title,
isCompleted: req.body.isCompleted ?? todos[targetIndex].isCompleted
}
const newTodos = [
...todos.slice(0, targetIndex),
updatedTodo,
...todos.slice(targetIndex + 1),
];
todos = newTodos
res.json(updatedTodo)
})
// Delete {id} Todo
app.delete("/api/todos/:id", (req, res) => {
const targetId = req.params.id
const targetIndex = todos.findIndex(todo => todo.id === targetId);
const isTargetTodoExist = targetIndex !== -1
if (!isTargetTodoExist) {
return res.status(404).json({ error: "Todo not found" });
}
const newTodos = [
...todos.slice(0, targetIndex),
...todos.slice(targetIndex + 1),
]
todos = newTodos
res.status(204).send();
})

3. Check

Popular API testing tools include: Postman🔗, Hoppscotch🔗, and Insomnia🔗. You can choose one that you find convenient for testing. I prefer Hoppscotch and have previously written a related recommendation article: Learn Hoppscotch🔗. You can create a group to save all the APIs related to this project for easier management.

Hoppscotch API Group

Side note: CORS Settings

Access to fetch at `http://localhost:3000/api/todos' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

If you encounter the above CORS error message in your frontend project, you can refer to another article I wrote: CORS for Web Developer🔗.

Literally, you can allow websites from different ports to use the API by setting the server response to include the allowed Access-Control-Allow-Origin. Here, I rely on setting up Express.js middleware: cors🔗 to solve this.

Terminal window
$ npm install cors

After installation, add the middleware and set the open origin:

app.use(cors({ origin: 'http://127.0.0.1:5500' }));

Side note: Using Nodemon to Restart the Server in Real-Time

If you find it troublesome to repeatedly restart the server after making changes during development, you can try Nodemon🔗.

Nodemon is used to automatically monitor file changes and restart the server while developing Node.js applications, avoiding the hassle of manually stopping and restarting the server. By adding it as an NPM Script🔗, you can run npm run dev to start the live-reloading development server.

"scripts": {
"dev": "nodemon app.js"
}

Summary

The project implemented this time: in-memory-todo🔗 files are available on GitHub, and I have additionally implemented the frontend part. If you are interested in the frontend, you can refer to another article of mine: Creating a Todo List in Five Steps with JavaScript🔗, which is basically built on this foundation by connecting to the backend data and avoiding some XSS🔗 vulnerabilities.

Further Reading