In today’s fast-paced world, staying organized is key. Whether it’s managing work tasks, personal errands, or creative projects, a well-structured to-do list can be a lifesaver. This tutorial will guide you through building a simple, yet functional, web-based to-do list application using Node.js, Express, and a touch of HTML, CSS, and JavaScript. We’ll cover everything from setting up your development environment to deploying your app, making it accessible from anywhere with an internet connection. This project is ideal for beginners and intermediate developers looking to expand their skills in backend development, web application design, and database interaction. You’ll learn practical skills that can be applied to a wide range of projects.
Why Build a To-Do List App?
Creating a to-do list application is a fantastic learning experience for several reasons:
- Practical Application: You’re building something useful that you can actually use daily.
- Full-Stack Exposure: You’ll touch on both frontend (HTML, CSS, JavaScript) and backend (Node.js, Express) technologies.
- Database Interaction: You’ll learn how to store and retrieve data, a fundamental skill for any web application.
- Modular Design: You’ll practice breaking down a complex problem into smaller, manageable parts.
- Real-World Relevance: To-do lists are a common feature in many applications, giving you a solid foundation for future projects.
Prerequisites
Before we dive in, ensure you have the following installed:
- Node.js and npm (Node Package Manager): This is the runtime environment and package manager for JavaScript. You can download it from nodejs.org.
- A Text Editor or IDE: Such as VS Code, Sublime Text, or Atom.
- Basic Understanding of HTML, CSS, and JavaScript: While we’ll cover the basics, a familiarity with these languages will be helpful.
Setting Up the Project
Let’s start by creating a new project directory and initializing our Node.js project. Open your terminal or command prompt and run the following commands:
mkdir todo-app
cd todo-app
npm init -y
This will create a new directory called todo-app, navigate into it, and initialize a package.json file with default settings. The package.json file will manage our project’s dependencies.
Installing Dependencies
Next, we need to install the necessary packages. We’ll use:
- Express: A web application framework for Node.js.
- ejs: A simple templating engine for generating HTML.
- body-parser: Middleware to parse request bodies.
- mongoose: An Object-Document Mapper (ODM) for MongoDB.
Run the following command in your terminal:
npm install express ejs body-parser mongoose
This command downloads and installs the packages, adding them to your project’s node_modules folder and updating your package.json file.
Project Structure
Before we start writing code, let’s establish a basic project structure:
todo-app/
├── node_modules/
├── views/
│ └── index.ejs
├── public/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── script.js
├── models/
│ └── todo.js
├── app.js
├── package.json
└── .gitignore
Here’s what each part does:
node_modules/: Contains the installed packages.views/: Contains our EJS template files for rendering HTML.public/: Contains static assets like CSS, JavaScript, and images.models/: Contains our Mongoose model definitions.app.js: The main application file where we’ll write our server-side code.package.json: Contains project metadata and dependencies..gitignore: Specifies files and folders that Git should ignore.
Creating the Server (app.js)
Let’s create our app.js file and set up a basic Express server:
// app.js
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const app = express();
const port = 3000;
app.use(express.static('public')); // Serve static files
app.set('view engine', 'ejs'); // Set view engine to EJS
app.use(bodyParser.urlencoded({ extended: true })); // Parse URL-encoded bodies
// Database connection
mongoose.connect('mongodb://localhost:27017/todoDB', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
// Routes (We'll define these later)
app.get('/', (req, res) => {
res.render('index'); // Render the index.ejs file
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Explanation:
- We import the necessary modules:
express,body-parser, andmongoose. - We create an Express application instance.
- We set up middleware to handle static files (CSS, JavaScript, images), set the view engine to EJS, and parse URL-encoded request bodies.
- We connect to a MongoDB database named
todoDB. Make sure you have MongoDB installed and running on your system. - We define a basic route (
/) that renders theindex.ejsfile. - We start the server and listen on port 3000.
Creating the EJS Template (views/index.ejs)
Now, let’s create our index.ejs file to define the HTML structure of our to-do list. Inside the views folder, create index.ejs with the following content:
<!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>
<div class="container">
<h1>To-Do List</h1>
<form action="/" method="POST">
<input type="text" name="task" placeholder="Add a task" required>
<button type="submit">Add</button>
</form>
<ul id="todoList">
<!-- Tasks will be displayed here -->
</ul>
</div>
<script src="/js/script.js"></script>
</body>
</html>
Explanation:
- We define the basic HTML structure, including the
<head>and<body>sections. - We include a link to our CSS file (
style.css) and a script for our JavaScript file (script.js). - We create a form to add new tasks. The form submits to the root route (
/) using the POST method. - We include an unordered list (
<ul>) with the IDtodoListwhere our to-do items will be displayed.
Creating the CSS File (public/css/style.css)
Let’s add some basic styling to our to-do list. Create a file named style.css inside the public/css/ directory with the following content:
body {
font-family: sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 80%;
max-width: 500px;
}
h1 {
text-align: center;
color: #333;
}
form {
display: flex;
margin-bottom: 20px;
}
input[type="text"] {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-right: 10px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
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;
}
li:last-child {
border-bottom: none;
}
.delete-button {
background-color: #f44336;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.delete-button:hover {
background-color: #da190b;
}
This CSS provides basic styling for the layout, form, and list items.
Creating the JavaScript File (public/js/script.js)
Now, let’s create a script.js file inside the public/js/ directory to handle the client-side interactions. For the initial version, we will focus on adding tasks and deleting them. Later, we will integrate it with our backend.
// script.js
const todoList = document.getElementById('todoList');
// Function to add a task to the list (placeholder)
function addTask(taskText) {
const li = document.createElement('li');
li.textContent = taskText;
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.classList.add('delete-button');
deleteButton.addEventListener('click', () => {
li.remove(); // Remove the list item when delete button is clicked
// You can add backend functionality to delete the task from DB here
});
li.appendChild(deleteButton);
todoList.appendChild(li);
}
// You will fetch tasks from the server and display them here
// Example: Add a task manually
// addTask('Example Task');
Explanation:
- We get a reference to the
todoListelement. - We define an
addTaskfunction that creates a new list item (<li>) and a delete button for each task. - The delete button removes the list item when clicked.
Creating the Mongoose Model (models/todo.js)
We’ll create a Mongoose model to define the structure of our to-do list items in the database. Create a file named todo.js inside the models/ directory with the following content:
// models/todo.js
const mongoose = require('mongoose');
const todoSchema = new mongoose.Schema({
task: {
type: String,
required: true,
},
completed: {
type: Boolean,
default: false,
},
// Add more fields if needed, e.g., dueDate, priority
});
module.exports = mongoose.model('Todo', todoSchema); // 'Todo' is the model name
Explanation:
- We import the
mongoosemodule. - We define a schema (
todoSchema) for our to-do items. It includes atask(string) and acompleted(boolean) field. - We create a Mongoose model named
Todobased on the schema. This model will allow us to interact with the database.
Implementing the Backend Logic (app.js – continued)
Now, let’s add the backend logic to handle adding tasks, retrieving tasks, and deleting tasks. Modify your app.js file as follows:
// app.js (continued)
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const app = express();
const port = 3000;
const Todo = require('./models/todo'); // Import the Todo model
app.use(express.static('public')); // Serve static files
app.set('view engine', 'ejs'); // Set view engine to EJS
app.use(bodyParser.urlencoded({ extended: true })); // Parse URL-encoded bodies
// Database connection
mongoose.connect('mongodb://localhost:27017/todoDB', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
// Routes
app.get('/', async (req, res) => {
try {
const todos = await Todo.find({}); // Retrieve all tasks from the database
res.render('index', { todos: todos }); // Pass the tasks to the template
} catch (err) {
console.error(err);
res.status(500).send('Error retrieving tasks');
}
});
app.post('/', async (req, res) => {
const newTask = req.body.task;
try {
const todo = new Todo({ task: newTask });
await todo.save(); // Save the new task to the database
res.redirect('/'); // Redirect back to the home page to refresh the list
} catch (err) {
console.error(err);
res.status(500).send('Error adding task');
}
});
app.post('/delete/:id', async (req, res) => {
const taskId = req.params.id;
try {
await Todo.findByIdAndDelete(taskId);
res.redirect('/');
} catch (err) {
console.error(err);
res.status(500).send('Error deleting task');
}
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Explanation:
- We import the
Todomodel. - We modify the root route (
/) to retrieve all to-do items from the database usingTodo.find({})and pass them to theindex.ejstemplate. - We add a POST route (
/) to handle adding new tasks. It creates a newTodoobject, saves it to the database, and redirects back to the home page. - We add a POST route (
/delete/:id) to handle deleting tasks. It finds a task by its ID and deletes it, then redirects back to the home page.
Updating the EJS Template (views/index.ejs – continued)
Now, let’s update our index.ejs file to display the tasks retrieved from the database and add delete buttons. Modify the index.ejs file:
<!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>
<div class="container">
<h1>To-Do List</h1>
<form action="/" method="POST">
<input type="text" name="task" placeholder="Add a task" required>
<button type="submit">Add</button>
</form>
<ul id="todoList">
<% todos.forEach(function(todo) { %>
<li>
<%= todo.task %>
<form action="/delete/<%= todo._id %>" method="POST" style="display: inline;">
<button type="submit" class="delete-button">Delete</button>
</form>
</li>
<% }); %>
</ul>
</div>
<script src="/js/script.js"></script>
</body>
</html>
Explanation:
- We use EJS syntax (
<% ... %>and<%= ... %>) to iterate over thetodosarray (passed from our server) and display each task. - Inside the
<ul>, we loop through each todo item. - We display the task text (
<%= todo.task %>). - We add a delete button for each task. The delete button submits a POST request to
/delete/:id, where:idis the ID of the task. The form’s style is set todisplay: inline;to keep the button on the same line as the task text.
Adding Functionality to the JavaScript File (public/js/script.js – continued)
With the backend and EJS template updated, we now need to integrate them with our frontend JavaScript file. We will no longer need to implement the addTask function, as our backend now handles the task addition and display.
We do not need to update script.js file as the backend takes care of retrieving and displaying the tasks. The list is dynamically rendered by EJS template using data fetched from the database.
Running the Application
Now that we’ve written all the necessary code, let’s run our application. In your terminal, navigate to your project directory (todo-app) and run the following command:
node app.js
This will start the Node.js server. Open your web browser and go to http://localhost:3000. You should see your to-do list application. You can now add tasks, and they will be saved in your MongoDB database. You can also delete tasks.
Common Mistakes and Troubleshooting
Here are some common mistakes and how to fix them:
- MongoDB Connection Errors:
- Error: The server fails to connect to MongoDB.
- Solution: Make sure MongoDB is installed and running. Double-check the connection string in
app.js. The default ismongodb://localhost:27017/todoDB. Also, verify that the MongoDB server is running on the default port (27017).
- Incorrect File Paths:
- Error: CSS or JavaScript files are not loaded.
- Solution: Double-check the file paths in your HTML (
index.ejs) and CSS. Ensure that the files are in the correct directories (public/css/andpublic/js/).
- Form Submission Issues:
- Error: Tasks are not being added or deleted.
- Solution: Verify the form’s
actionandmethodattributes inindex.ejs. Make sure the backend routes (/for adding,/delete/:idfor deleting) are correctly defined inapp.js. Check the browser’s developer console for any errors.
- EJS Syntax Errors:
- Error: The server fails to render the EJS template.
- Solution: Carefully check your EJS syntax (
<% ... %>and<%= ... %>). Make sure you’re passing the correct data to the template from your server.
- Missing Dependencies:
- Error: The server throws an error because a module is not found.
- Solution: Ensure you have installed all the required dependencies using
npm install. Check yourpackage.jsonfile to verify that all necessary packages are listed.
Key Takeaways
In this tutorial, you’ve learned how to build a basic to-do list application using Node.js, Express, EJS, MongoDB, HTML, CSS, and JavaScript. You’ve gained experience with:
- Setting up a Node.js and Express project.
- Creating EJS templates for dynamic content.
- Using Mongoose to interact with a MongoDB database.
- Handling form submissions and routing.
- Serving static assets like CSS and JavaScript.
- Designing a basic user interface.
FAQ
Here are some frequently asked questions:
- Can I deploy this application to the cloud? Yes, you can deploy this application to platforms like Heroku, AWS, or Google Cloud. You’ll need to configure your environment variables for MongoDB connection and other settings.
- How can I add user authentication? You can integrate user authentication using libraries like Passport.js or implement your own authentication system. This involves creating user models, handling registration and login, and securing routes.
- How can I add more features (e.g., due dates, priorities)? You can extend the
Todomodel inmodels/todo.jsto include additional fields, such asdueDate(Date),priority(String), and implement corresponding changes in your EJS template and backend routes. - What are some best practices for larger applications? For larger applications, consider using a more structured architecture (e.g., MVC), implementing more robust error handling, and using a more sophisticated database management strategy.
This tutorial provides a solid foundation for your web development journey. You can now adapt this project and add features to create more complex and personalized applications. Consider experimenting with new features, such as marking tasks as complete, setting due dates, and implementing user authentication. The skills you’ve acquired will serve as a valuable asset as you continue to explore the world of web development. Take the initiative to learn, experiment, and build – that’s how you truly master these technologies.
