Build a To-Do List with Node.js, Express, and Vanilla JavaScript

Computer Science

Build a To-Do List with Node.js, Express, and Vanilla JavaScript

Node.jsExpressJavaScript

Build a To-Do List with Node.js, Express, and Vanilla JavaScript

Build a small working to-do list from scratch. You will use familiar HTML structure and CSS selectors while learning the missing pieces: a Node.js project, an Express server, a tiny JSON API, and vanilla JavaScript that updates the page when tasks are added or completed.

2 modules6 lessonsComputer ScienceNode.jsExpressJavaScriptHTMLCSSnpmFetch API

What you learn by building this

  • Create and run a Node.js project with an Express server
  • Serve an HTML, CSS, and JavaScript frontend from Express
  • Design a small API for reading, creating, and completing to-do items
  • Use browser fetch requests and DOM updates to keep the interface synchronized with the server
  • Run and demonstrate a complete working to-do list locally

Learning Journey

1

Make the to-do page stand on its own

2 lessons

Create the project folder and a polished static interface first, using the learner's existing HTML and CSS foundation. The page will be runnable before any server or API work begins.

2

Add the Express server and connect the browser

4 lessons

Turn the static page into a running Node.js application, then replace hard-coded task behavior with a tiny server-backed API.

Public lesson

Create the task page

Build the task page

You already know how to organize a document semantically. Now use that skill inside your React app to make the task page feel like a real interface before adding data or backend behavior.

Tasks

1. Inspect the page you already have

Open the component that currently renders the main page of your React app.

Before changing it, find:

  • the component that should become the task page
  • the stylesheet it already uses
  • the app’s existing heading or navigation structure
  • whether the project already has a Task type, array, or mock data

Do not create a second app entry point. This page should fit into the structure you have already built.

Tasks

2. Sketch the page in semantic regions

Your page needs these visible regions:

  1. A page heading and short description
  2. A form for adding a task
  3. A task list
  4. An empty-state message when there are no tasks
  5. A completion control for each task

Use elements for their meaning:

  • <main> for the page’s primary content
  • <form> for task creation
  • <label> connected to the text input
  • <ul> and <li> for the list
  • <input type="checkbox"> for completion
  • <button> for actions

Do not use a group of <div> elements where one of these elements describes the content more accurately.

Tasks

3. Add the page structure

Adapt this scaffold to the component and data names in your project. The missing expressions are intentional: you will connect them to your own state or mock data.

<!-- tangeble:starter -->
export default function TaskPage() {
  // Use the task data your project already has.
  // For now, it can be a small local array if no data source exists yet.
  const tasks = /* your task collection */;

  return (
    <main>
      <header>
        <h1>My tasks</h1>
        <p>Keep track of what needs to get done.</p>
      </header>

      <section aria-labelledby="add-task-heading">
        <h2 id="add-task-heading">Add a task</h2>

        <form /* connect the submit behavior later */>
          <label htmlFor="task-title">Task name</label>
          <input
            id="task-title"
            name="title"
            type="text"
            placeholder="What needs to be done?"
            /* add the value/change connection used by your app */
          />
          <button type="submit">Add task</button>
        </form>
      </section>

      <section aria-labelledby="task-list-heading">
        <h2 id="task-list-heading">Tasks</h2>

        {tasks.length === 0 ? (
          <p role="status">You have no tasks yet.</p>
        ) : (
          <ul>
            {tasks.map((task) => (
              <li key={/* use the task's stable id */}>
                <label>
                  <input
                    type="checkbox"
                    checked={/* use the task's completion value */}
                    onChange={/* connect the completion behavior later */}
                  />
                  <span>{/* render the task title */}</span>
                </label>
              </li>
            ))}
          </ul>
        )}
      </section>
    </main>
  );
}

For the first pass, it is fine if the form and checkbox do not change data yet. The important part is that the browser can render the complete interface and the controls have the right meaning.

Tasks

Check

Save the file and open the app in the browser.

You should see:

  • one clear page heading
  • an “Add a task” form
  • a task list if your collection has items
  • the empty-state sentence if the collection is empty
  • a checkbox beside each rendered task

If the page is blank, check the browser console first. A common cause is using a property name that does not match your existing task objects.

Tasks

4. Make the empty state switch visibly

Temporarily set your task collection to an empty array, using the place where your current mock data lives.

Do not remove the list markup. The conditional should decide which message the user sees.

The browser should now show:

Tasks
You have no tasks yet.

It should not show an empty <ul> with no explanation.

Restore your sample task afterward so you can check both states.

Tasks

5. Connect the completion control

Find the property in your task objects that represents completion. It may be called something like completed, done, or isComplete.

Connect that property to the checkbox:

  • checked should reflect the task’s current value
  • onChange should call the update path your app already uses, if one exists
  • the task title should remain visible beside the control

If completion behavior does not exist yet, leave the handler as a clearly named placeholder rather than inventing a backend request. The control must still render with the correct checked or unchecked state.

Tasks

<!-- tangeble:starter -->
function TaskRow({ task, onToggle }) {
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={/* task's completion value */}
          onChange={() => onToggle(/* task's stable id */)}
        />
        <span>{/* task title */}</span>
      </label>
    </li>
  );
}

Use this row only if extracting it makes your existing page easier to read. Otherwise, keep the row inside the list. Since your React component experience is still developing, prefer the simpler version that keeps the data flow easy to follow.

Tasks

Check

In the browser, inspect at least two tasks:

  • an incomplete task should have an unchecked box
  • a completed task should have a checked box
  • each checkbox should be clickable without clicking the text or another task

If you have connected the update behavior, click a checkbox and confirm its visual state changes. If persistence is not built yet, a refresh may reset it; that is okay for this step.

Tasks

6. Verify the form’s structure

Click the input’s visible label. Focus should move into the text field.

Then type a task title and press Enter.

If your submit logic is not implemented yet, the browser may submit or reload. Preventing submission and adding a task will come in the next behavior step. For now, confirm that:

  • the input has a visible label
  • the button is inside the form
  • the button has type="submit"
  • the form contains only controls that belong to adding a task

Finally, use the browser’s accessibility tree or inspect the rendered HTML. You should be able to identify the page as a <main>, the task collection as a <ul>, and every task as an <li>.

Course Outline

2 modules · 6 lessons

Make the to-do page stand on its own

Add the Express server and connect the browser

Learn by building your own version.

Remix this public project to open the workspace, follow the guided build, and let the AI mentor teach you through the work instead of doing it for you.