Building a Basic Todo List App with Express.js

Learn Web Dev Fundamentals

It supports the following tasks and endpoints:

  • Retrieve all todos: GET /todos

  • Retrieve a specific todo by ID: GET /todos/:id

  • Create a new todo: POST /todo

  • Update an existing todo by ID: PUT /todos/:id

  • Delete a todo by ID: DELETE /todos/:id

The todos are stored persistently in a JSON file, and the server uses middleware to parse incoming JSON requests.

Project Setup

First, initialize the project with basic requirements

npm init -y
npm install express

Create a file named todolist.json to store the todos.


Middleware: Parsing JSON Requests

Configure middleware to parse JSON request bodies.

// Imports
import express from 'express';
import { promises as fs } from 'fs';

const app = express();
const PORT = 3000;

app.use(express.json());

This allows the server to handle incoming JSON data.


File-Based Storage for Persistence

Task: Load Todos from File

// Path to the JSON file
const filePath = 'todolist.json';

let todoArr = [];
async function loadTodosFromFile() {
    try {
        const data = await fs.readFile(filePath, 'utf-8');
        todoArr = JSON.parse(data);
        console.log('Todos loaded:', todoArr);
    } catch (error) {
        console.error('Error loading todos:', error.message);
        throw new Error('Something went wrong!');
    }
}

// Initialize todos on startup
await loadTodosFromFile()
console.log('Initial todo file: ', todoArr);

This ensures todos are reloaded from todolist.json when the server starts.

Task: Save Todos to File

async function saveTodosToFile() {
    try {
        await fs.writeFile(filePath, JSON.stringify(todoArr, null, 2), 'utf-8');
        console.log('Todos saved successfully.');
    } catch (error) {
        console.error('Error saving todos:', error.message);
    }
}

This function writes the current state of todos back to todolist.json.


RESTful Endpoints

Retrieve All Todos

Endpoint: GET /todos

app.get('/todos', (req, res) => {
    res.status(200).json(todoArr);
});

Fetches all Todo items.

Retrieve Todo by ID

Endpoint: GET /todos/:id

app.get('/todos/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const todo = todoArr.find((t) => t.id === id);
    if (!todo) {
        return res.status(400).send('Todo not found!');
    }
    res.status(200).json(todo);
});

Finds a specific Todo by its ID.

Create a New Todo

Endpoint: POST /todo

app.post('/todo', async (req, res) => {
    const { title, description } = req.body;
    if (!title || !description) {
        return res.status(400).send('Title and description are required!');
    }
    const newTodo = {
        id: todoArr.length + 1,
        title,
        description
    };
    todoArr.push(newTodo);
    await saveTodosToFile();
    res.status(200).json({
        message: 'Todo added successfully.',
        status: 'success',
        todo: newTodo
    });
});

Creates a new Todo item and assigns it a unique ID.

Update an Existing Todo

Endpoint: PUT /todos/:id

app.put('/todos/:id', async (req, res) => {
    const { title, description } = req.body;
    const id = parseInt(req.params.id);
    const todoIndex = todoArr.findIndex((t) => t.id === id);

    if (todoIndex === -1) {
        return res.status(400).send('Todo not found!');
    }

    if (title) todoArr[todoIndex].title = title;
    if (description) todoArr[todoIndex].description = description;

    await saveTodosToFile();
    res.status(200).json({
        message: 'Todo updated successfully.',
        status: 'success',
        todo: todoArr[todoIndex]
    });
});

Updates the title or description of an existing Todo.

Delete a Todo

Endpoint: DELETE /todos/:id

app.delete('/todos/:id', async (req, res) => {
    const id = parseInt(req.params.id);
    const todoIndex = todoArr.findIndex((t) => t.id === id);

    if (todoIndex === -1) {
        return res.status(400).send('Todo not found!');
    }

    const deletedTodo = todoArr.splice(todoIndex, 1)[0];

    // Reassign IDs after deletion
    todoArr = todoArr.map((todo, index) => ({
        ...todo,
        id: index + 1
    }));

    await saveTodosToFile();
    res.status(200).json({
        message: 'Todo deleted successfully.',
        status: 'success',
        todo: deletedTodo
    });
});

Deletes a Todo by its ID and reassigns IDs to remaining Todos.


Starting the Server

app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});

Starts the server and listens for incoming requests.


Testing

Run the server and test using tools like Postman or curl. Example:

curl http://localhost:3000/todos