Build a Node.js Interactive Web-Based Simple To-Do List

Written by

in

Are you tired of juggling multiple tasks, appointments, and reminders in your head? Do you find yourself constantly forgetting important things, leading to missed deadlines and increased stress? In today’s fast-paced world, staying organized is crucial for productivity and peace of mind. A simple, yet effective, to-do list application can be a game-changer.

This tutorial will guide you through building your own interactive, web-based to-do list using Node.js. We’ll start from scratch, covering everything from setting up your development environment to deploying your application. By the end of this tutorial, you’ll have a fully functional to-do list that you can use to manage your tasks, track your progress, and boost your productivity. This project is perfect for beginners and intermediate developers looking to expand their knowledge of Node.js and web development.

What We’ll Build

We’ll create a simple, yet practical to-do list application with the following features:

  • Adding new tasks
  • Marking tasks as complete
  • Deleting tasks
  • Viewing all tasks
  • User-friendly interface (using HTML, CSS, and JavaScript)

Prerequisites

Before we begin, make sure you have the following installed on your system:

  • Node.js and npm (Node Package Manager)
  • A code editor (e.g., Visual Studio Code, Sublime Text, Atom)
  • Basic knowledge of HTML, CSS, and JavaScript is helpful but not strictly required.

Setting Up Your Project

Let’s start by setting up our project directory and initializing our Node.js project.

  1. Create a new directory for your project (e.g., todo-app).
  2. Open your terminal or command prompt and navigate to your project directory.
  3. Initialize a new Node.js project using npm:
npm init -y

This command creates a package.json file, which will manage our project dependencies.

Installing Dependencies

We’ll need a few dependencies for our project:

  • express: A web application framework for Node.js.
  • ejs: A simple templating engine for generating HTML.
  • (Optional) body-parser: Middleware to parse request bodies (although modern Express versions often include this functionality).

Install these dependencies using npm:

npm install express ejs

Project Structure

Let’s create the following basic file structure for our project:

todo-app/
├── index.js          # Main application file
├── views/
│   └── index.ejs     # EJS template for the to-do list
├── public/
│   ├── css/
│   │   └── style.css   # CSS styles
│   └── js/
│       └── script.js  # JavaScript for client-side interactions (optional)
├── package.json

Creating the Server (index.js)

Now, let’s write the code for our server (index.js). This file will handle incoming requests and serve the to-do list.

// index.js
const express = require('express');
const app = express();
const port = 3000;

// Middleware to parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));

// Set the view engine to EJS
app.set('view engine', 'ejs');

// Serve static files (CSS, JavaScript, images)
app.use(express.static('public'));

// In-memory data store for tasks
let tasks = [];

// Routes
app.get('/', (req, res) => {
  res.render('index', { tasks: tasks });
});

app.post('/add', (req, res) => {
  const newTask = req.body.task;
  if (newTask) {
    tasks.push(newTask);
  }
  res.redirect('/');
});

app.post('/delete', (req, res) => {
  const taskToDelete = req.body.task;
  tasks = tasks.filter(task => task !== taskToDelete);
  res.redirect('/');
});

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

Let’s break down this code:

  • We import the express module and create an Express application instance.
  • We set the port to 3000.
  • app.use(express.urlencoded({ extended: true }));: This middleware is crucial for parsing data sent from HTML forms (like our “Add Task” form). The extended: true option allows for more complex data structures.
  • app.set('view engine', 'ejs');: This configures Express to use EJS as our templating engine.
  • app.use(express.static('public'));: This line serves static files (CSS, JavaScript, images) from the public directory.
  • tasks array: This is our in-memory data store for the to-do list items. In a real-world application, you would likely use a database to persist the data.
  • app.get('/'): This route handles GET requests to the root path (/). It renders the index.ejs template and passes the tasks data to it.
  • app.post('/add'): This route handles POST requests to the /add path. It extracts the new task from the request body, adds it to the tasks array, and redirects the user back to the root path.
  • app.post('/delete'): This route handles POST requests to the /delete path. It extracts the task to delete from the request body, filters it from the tasks array, and redirects the user back to the root path.
  • app.listen(port, ...): This starts the server and listens for incoming requests on the specified port.

Creating the EJS Template (views/index.ejs)

Now, let’s create the EJS template (views/index.ejs) that will render our to-do list.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>To-Do List</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <h2>To-Do List</h2>

    <form action="/add" method="POST">
        <input type="text" name="task" placeholder="Add a task">
        <button type="submit">Add</button>
    </form>

    <ul>
        <% tasks.forEach(task => { %>
            <li>
                <%- task %>
                <form action="/delete" method="POST" style="display: inline;">
                    <input type="hidden" name="task" value="<%- task %>">
                    <button type="submit">Delete</button>
                </form>
            </li>
        <% }); %>
    </ul>

    <script src="/js/script.js"></script>
</body>
</html>

Here’s what this template does:

  • It defines the basic HTML structure, including the title and a link to our CSS file.
  • It includes a form for adding new tasks. The form’s action is set to /add and the method is POST. The name attribute of the input field is set to task, which is how we’ll access the input value in our server-side code.
  • It iterates through the tasks array (passed from the server) and displays each task in a list item (<li>).
  • It includes a delete button next to each task. This button submits a form to the /delete route. The <input type="hidden" name="task" value="<%- task %>"> sends the task’s text as a hidden field in the POST request. The inline style `style=”display: inline;”` ensures the delete button appears next to the task text.
  • It links to an optional JavaScript file (script.js) for client-side interactions.

Adding Styles (public/css/style.css)

Let’s add some basic CSS to style our to-do list. Create a file named style.css inside the public/css directory and add the following:

body {
    font-family: sans-serif;
    margin: 20px;
}

h2 {
    margin-bottom: 10px;
}

form {
    margin-bottom: 20px;
}

input[type="text"] {
    padding: 5px;
    margin-right: 5px;
    border: 1px solid #ccc;
    border-radius: 4px;
}

button {
    padding: 5px 10px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

button:hover {
    background-color: #3e8e41;
}

ul {
    list-style: none;
    padding: 0;
}

li {
    padding: 10px;
    border-bottom: 1px solid #eee;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

Running the Application

Now that we have all the necessary files, let’s run our application.

  1. Open your terminal or command prompt and navigate to your project directory.
  2. Start the server by running the following command:
node index.js

You should see the message “Server is running on http://localhost:3000” in your console.

  1. Open your web browser and go to http://localhost:3000.
  2. You should see your to-do list application. You can now add tasks, and they will be displayed on the page.
  3. To delete a task, click the Delete button next to it.

Adding Client-Side Interactivity (public/js/script.js)

While our to-do list is functional, it currently refreshes the entire page every time you add or delete a task. Let’s add some basic JavaScript to make it more interactive and prevent full page reloads. Create a file named script.js inside the public/js directory and add the following:

// script.js

// Prevent form submissions from refreshing the page
const addForm = document.querySelector('form');

if (addForm) {
    addForm.addEventListener('submit', (event) => {
        event.preventDefault(); // Prevent the default form submission

        // Get the task from the input field
        const taskInput = addForm.querySelector('input[name="task"]');
        const task = taskInput.value;

        if (task) {
            // Send a POST request to the server
            fetch('/add', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: `task=${task}`,
            })
            .then(() => {
                // Reload the page after adding the task (for simplicity).  A more advanced solution would update the DOM directly.
                location.reload();
            })
            .catch(error => console.error('Error adding task:', error));

            // Clear the input field
            taskInput.value = '';
        }
    });
}

// Add event listeners to delete buttons
const deleteButtons = document.querySelectorAll('form[action="/delete"]');

deleteButtons.forEach(deleteForm => {
    deleteForm.addEventListener('submit', (event) => {
        event.preventDefault();

        const taskToDelete = deleteForm.querySelector('input[name="task"]').value;

        fetch('/delete', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: `task=${taskToDelete}`,
        })
        .then(() => {
            location.reload();
        })
        .catch(error => console.error('Error deleting task:', error));
    });
});

Let’s break down the JavaScript code:

  • The code selects the form element.
  • It attaches an event listener to the form’s submit event. When the form is submitted (i.e., the “Add” button is clicked), the event listener is triggered.
  • event.preventDefault();: This line prevents the default form submission behavior, which would cause a full page reload.
  • The code gets the task from the input field.
  • It sends a POST request to the /add endpoint using the fetch API. The request includes the task as part of the request body.
  • After a successful request, the page reloads. In a more complex application, you’d update the DOM directly to avoid a full reload.
  • The input field is cleared.
  • Similar logic is used for the delete buttons. The code selects all delete form elements, adds event listeners to their submit events, and sends a POST request to the /delete endpoint when a delete button is clicked.

To enable the JavaScript, make sure you’ve included the script tag in your index.ejs file (as shown earlier): <script src="/js/script.js"></script>

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Server Not Running: Make sure your Node.js server is running in the terminal. If you make changes to your server-side code (index.js), you’ll need to restart the server for the changes to take effect.
  • Incorrect File Paths: Double-check the file paths in your HTML, CSS, and JavaScript files. Make sure they match the file structure you created. For instance, the <link rel="stylesheet" href="/css/style.css"> tag in index.ejs assumes that your CSS file is located in the public/css directory.
  • Missing Dependencies: Make sure you’ve installed all the necessary dependencies using npm install. If you get an error message about a missing module, try running npm install <module-name>.
  • Form Data Not Being Sent: Ensure that the name attributes of your form input fields match the names you’re using to access the data in your server-side code (e.g., <input type="text" name="task">). Also, verify you have the app.use(express.urlencoded({ extended: true })); middleware set up to parse form data.
  • EJS Errors: If you see errors related to EJS, check your EJS syntax and make sure you’re passing the correct data from your server to the template.
  • CORS (Cross-Origin Resource Sharing) Issues: If you’re running your front-end and back-end on different ports, you might encounter CORS errors. You can usually fix this by enabling CORS on your server. For this simple app, it’s less likely to be an issue, but be aware of it for more complex applications. You can install and use the `cors` npm package.
  • JavaScript Errors: Use your browser’s developer console (usually accessed by right-clicking on the page and selecting “Inspect” or “Inspect Element”) to check for JavaScript errors. These errors can provide valuable clues about what’s going wrong.

Key Takeaways

  • You’ve learned how to create a basic web application using Node.js and Express.
  • You’ve seen how to handle HTTP requests (GET and POST).
  • You’ve learned how to use EJS to generate HTML dynamically.
  • You’ve gained experience with client-side JavaScript to make your application more interactive.
  • You’ve learned the basic structure of a Node.js project.

FAQ

Here are some frequently asked questions about this project:

  1. Can I store the tasks in a database? Yes, absolutely! Using a database (like MongoDB, PostgreSQL, or MySQL) is the standard way to persist data in real-world applications. You would need to install a database client library (e.g., mongoose for MongoDB) and modify your server-side code to interact with the database.
  2. How can I deploy this application? You can deploy your application to platforms like Heroku, Netlify, or AWS. You’ll need to choose a platform, configure it to run your Node.js application, and possibly configure a database if you’re using one.
  3. How can I add features like marking tasks as complete? You can add a checkbox or a button next to each task. When the user interacts with the checkbox or button, you can send a POST request to the server to update the task’s status in the tasks array (or in your database). You would need to modify your EJS template and your server-side code.
  4. How can I add more advanced features, such as due dates or priorities? You can extend your data model (e.g., by adding dueDate and priority properties to your task objects). You would need to modify your HTML form to collect the additional information, update your server-side code to handle the new data, and modify your EJS template to display the new information.
  5. How can I improve the user interface? You can use CSS frameworks like Bootstrap or Tailwind CSS to quickly style your application. You can also use JavaScript frameworks like React, Angular, or Vue.js for more complex and interactive user interfaces.

This tutorial provides a solid foundation for building a simple to-do list application. You can extend this project by adding more features and improving the user interface. Remember to practice regularly and experiment with different technologies to enhance your skills.

With this basic to-do list, you’ve taken the first step towards building more complex web applications. The concepts of handling requests, rendering dynamic content, and interacting with forms are fundamental to web development. Keep practicing, keep learning, and don’t be afraid to experiment with new technologies and features to grow your skills. The journey of a thousand lines of code begins with a single task added to your list. Now go forth and organize your life, one task at a time!