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

JavaScriptNode.jsExpress

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

You’ll build a small working to-do list from scratch: an Express server delivers the page and handles a tiny API, while browser JavaScript lets you add, complete, and remove tasks without reloading. You already have a foundation in HTML structure, CSS selectors, and guided React components, so this course uses those skills directly while introducing the server-and-browser connection you need for this focused project.

3 modules9 lessonsComputer ScienceJavaScriptNode.jsExpressHTMLCSSFetch API

What you learn by building this

  • Set up and run a small Node.js Express application
  • Serve a semantic HTML, CSS, and vanilla JavaScript interface from Express
  • Design and use a small JSON API for listing, adding, completing, and deleting tasks
  • Connect browser event handlers to fetch requests and update the visible task list
  • Handle empty states, invalid input, and server errors clearly
  • Demonstrate the finished application through repeatable observable behaviors

Learning Journey

1

Create the standalone to-do interface

3 lessons

Build the visible application shell first, using the learner’s existing HTML and CSS strengths. By the end, the project is a usable static interface with task rows, controls, and an empty state before any server work begins.

2

Put the interface behind an Express server

3 lessons

Move from a browser-only prototype to a one-process Node.js application. The learner adds Express gradually, keeps the familiar static page working, and exposes a tiny in-memory API that the browser can call.

3

Finish the task lifecycle and polish the demo

3 lessons

Complete the core CRUD behavior and make the finished project reliable to show. The learner adds completion and deletion endpoints, handles ordinary failure cases, and verifies the whole flow from a clean start.

Public lesson

Plan the task shape and page shell

Lesson: Shape the task data and page shell

This lesson gives the to-do app a clear structure before adding behavior. You will decide what one task looks like, then use that shape to build a page with:

  • a page heading
  • a task form
  • a task list
  • an empty state when there are no tasks

The goal is not just to make the page look like a to-do app. It is to make the page’s structure communicate its meaning to the browser, assistive technology, and your future React code.

Your HTML structure is already a strength, so we will use that carefully while keeping the React part small and explicit.

Predict

What will happen?

1. Predict the structure

Before opening the file, think about this question:

If someone could not see the styling, which elements would still tell them what this page is?

A reasonable outline is:

main
├── h1
├── form
│   ├── label
│   ├── input
│   └── button
└── section
    ├── heading
    └── list or empty-state message

The important distinction is between appearance and meaning:

  • A div can group content, but it does not explain what the group is.
  • A form tells the browser that the user is entering information to submit.
  • A ul tells the browser that several items belong to one list.
  • A label tells the user what an input is for.
  • A heading gives the page and its task area a navigable outline.

This structure will still matter later when you add state, filtering, and event handlers. If the shell is vague now, those features have to work around that vagueness.

Tasks

2. Decide what one task contains

Open the existing React page or component that is intended for this lesson. If you are unsure which file to edit, inspect the project’s existing page/component files and follow the project’s documented development command rather than creating a new project.

Start with the smallest useful task shape. A task needs:

const exampleTask = {
  id: /* stable identifier */,
  title: /* text shown to the user */,
  completed: /* true or false */
};

Use these meanings:

  • id identifies the task independently of its position in the array.
  • title is the user-facing task text.
  • completed describes whether the task is finished.

The id matters because a task’s position can change when tasks are sorted, deleted, or filtered. React also needs a stable identity when rendering a list. The title should contain the task’s content, not markup. The completed value should be a boolean, not a string such as "false".

Now create a small collection using your own task text. Keep it to one or two objects for now:

const initialTasks = [
  {
    id: /* your first id */,
    title: /* your first task title */,
    completed: false
  }
  // Add a second task only if it helps you inspect the list layout.
];

Do not add fields such as priority, dueDate, or createdBy yet. Extra fields create decisions that the page does not use. A small shape is easier to understand and easier to change.

Checkpoint

Pause and inspect your data. Explain to yourself:

  1. Which property would React use to distinguish two tasks?
  2. Which property would be displayed?
  3. Which property could later control a completed style or checkbox?

If you cannot answer one of these, adjust the object before building the page.

Tasks

3. Build the outer page shell

In the relevant component, replace or extend the current page markup with a semantic outer structure. Use a skeleton like this, but choose the exact text and class names yourself:

return (
  <main className="todo-page">
    <header>
      <h1>{/* page title */}</h1>
      <p>{/* short description of what the page does */}</p>
    </header>

    {/* task form goes here */}

    <section aria-labelledby="task-list-heading">
      <h2 id="task-list-heading">{/* task section heading */}</h2>

      {/* task list or empty state goes here */}
    </section>
  </main>
);

The aria-labelledby connection is deliberate. It tells assistive technology that the section’s accessible name comes from the h2 with that ID. The visible heading and the accessibility information stay synchronized instead of describing the section twice.

Use a header for introductory content, but do not put every group inside a header. The main element should represent the page’s primary content, and the section should represent the distinct task area.

Visible check

Run the project using its documented command and open the page in the browser.

Then inspect the page with developer tools:

  • Is there exactly one primary main region for this page?
  • Does the page have a clear h1?
  • Does the task area have its own heading?
  • If you temporarily disable CSS, does the content still have a sensible order?

If the page is blank or the compiler reports an error, read the first error from the top. JSX errors often cause later lines to appear broken even though the real problem is earlier.

Tasks

4. Add the task form

A form should describe the user’s action, even before submission works. Add the form inside the page shell:

<form>
  <label htmlFor="task-title">
    {/* label text */}
  </label>

  <input
    id="task-title"
    name="task-title"
    type="text"
    placeholder={/* short example, if useful */}
  />

  <button type="submit">
    {/* action text */}
  </button>
</form>

Adapt the missing values to your page.

Three details are easy to overlook:

  • htmlFor must match the input’s id. In JSX, use htmlFor, not HTML’s for.
  • name gives the field a meaningful form control name.
  • type="submit" makes the button’s purpose explicit.

Do not add a submit handler yet unless the existing project requires one to prevent a page reload. At this stage, the form’s job is to establish the interface and its relationships, not to pretend that task creation is complete.

A placeholder is not a label. A placeholder disappears when the user types and may be difficult to read. Keep the visible label.

Visible check

Use the browser to test the form structurally:

  1. Click the label. Does focus move to the input?
  2. Press Tab. Does focus reach the input and then the button in a sensible order?
  3. With the input empty, what happens when you activate the button?

The last observation is useful even if you do not implement validation yet. The browser may submit the form or report no constraint; that behavior is a reminder that visual markup and interaction behavior are separate concerns.

Tasks

5. Render the task list from the data shape

The task area should represent a collection as a real list. Use the existing initialTasks array rather than writing unrelated markup for every task.

A partial rendering shape could look like this:

<ul className="task-list">
  {initialTasks.map((task) => (
    <li key={/* stable task identifier */}>
      <label>
        <input
          type="checkbox"
          checked={/* task completion value */}
          readOnly
        />
        <span>{/* task title */}</span>
      </label>
    </li>
  ))}
</ul>

Fill in the expressions using the properties of your task object.

Notice the relationship:

  • The array describes what tasks exist.
  • .map() turns each data object into one list item.
  • key gives React a stable identity for that list item.
  • The checkbox reflects completed.
  • The text reflects title.

readOnly is appropriate for this static shell if the checkbox is controlled by a value but does not yet have a change handler. Without it, React may warn that the field is controlled but cannot be changed. You are not implementing completion behavior in this lesson; you are making the relationship visible.

Do not use the array index as the key just because it is available. If a task is removed later, the remaining tasks can shift positions, and index keys no longer represent the same task.

Visible check

With one or two tasks in initialTasks, verify:

  • each object produces exactly one li
  • the visible title matches the object’s title
  • a task with completed: true starts checked
  • the browser console has no React key warning

Change one task’s completed value and reload. The visible checkbox should change without changing the rendering code. That demonstrates that the page is using the data shape rather than hardcoded task markup.

Tasks

6. Add the empty state

A task list also needs to explain what is happening when the collection is empty. Otherwise the user may see a heading followed by blank space and wonder whether the page failed.

Render either the list or an empty-state message based on the number of tasks:

{initialTasks.length > 0 ? (
  <ul className="task-list">
    {/* map the tasks here */}
  </ul>
) : (
  <p className="empty-state">
    {/* explain that there are currently no tasks */}
  </p>
)}

The condition is based on the collection, not on a separate hasTasks value. That avoids two pieces of information drifting apart.

Temporarily change your data to:

const initialTasks = [];

Do not leave the list’s old static item outside the conditional. If you do, the empty state and the task item will appear together, which makes the page contradict itself.

Visible check

Reload with an empty array and confirm that:

  • the task list is absent
  • the empty-state message is visible
  • the form and page headings remain visible
  • the page still has a logical reading order

Then restore one task and confirm that the list returns. This is an important check: both branches should be valid page states, not just one successful screenshot.

Explain it back

Put it in your own words

7. Review the completed shell

Your component should now have the following conceptual shape:

main
├── header
│   ├── h1
│   └── description
├── form
│   ├── label + input
│   └── submit button
└── section
    ├── h2
    └── one of:
        ├── ul > li for each task
        └── empty-state paragraph

Use the browser and the DOM inspector to verify the actual elements, not just how they look.

Finally, explain the page in your own words:

  • Why is the task collection an array of objects instead of separate variables?
  • Why does the list use key={task.id} rather than the array index?
  • Why should the empty state be a separate branch?
  • Which element tells a user what the input is for?
  • Which heading names the task section?

If the answers are clear, the shell is doing more than displaying text: it has established a reliable contract between the task data, the React render, and the page’s semantic structure.

20 more characters

Course Outline

3 modules · 9 lessons

Create the standalone to-do interface

Put the interface behind an Express server

Finish the task lifecycle and polish the demo

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.