Build a Small To-Do App with Express and Vanilla JavaScript
Computer Science
Build a Small To-Do App with Express and Vanilla JavaScript
Build a Small To-Do App with Express and Vanilla JavaScript
You’ll turn your existing HTML and CSS foundation into a small working to-do app. You’ll add a focused Node.js Express server, connect the browser to a tiny JSON API, and finish with a project you can run locally, demonstrate, and extend.
What you learn by building this
- Create and run a minimal Express server from a single Node.js entry file
- Design a small in-memory task data model and expose it through predictable HTTP routes
- Use browser fetch requests to read and change task data
- Render task state into an existing semantic HTML page using vanilla JavaScript
- Handle form submission, completion toggles, deletion, empty states, and basic request errors
Learning Journey
Shape the standalone to-do page
2 lessonsStart from the learner’s existing HTML foundation and make the interface usable on its own before introducing the server. The module ends with a static page that clearly shows the intended task states.
Add the smallest Express server
3 lessonsBuild the server in a narrow step: first make Node run, then serve the already-working page, then add one predictable API response. Each lesson leaves the project runnable.
Connect the browser to task actions
4 lessonsUse the reference project to wire the finished interface to the server one action at a time. The learner reuses familiar component-style thinking without introducing React or a second runtime.
Public lesson
Define the task interface
Build the to-do screen structure
Your page is ready for a focused interface. Before adding JavaScript, give the browser a small, predictable structure that can later support adding, completing, and removing tasks.
Predict
What will happen?
First, look at the current page and predict: if the styling disappeared, would a user still be able to tell where to enter a task and where the tasks belong?
Tasks
1. Replace the page content with a semantic skeleton
Open the React component that currently renders the page. Keep its existing outer setup, but replace the page-specific content with a to-do interface.
Use one main region, one form for adding tasks, and one section for the task list. The id values will give later JavaScript direct targets.
export default function App() {
return (
<main className="todo-app">
<header className="todo-header">
<h1>Today's tasks</h1>
{/* Add a short description or task count here. */}
</header>
<form id="task-form" className="task-form">
<label htmlFor="task-input">New task</label>
<div className="task-entry">
<input
id="task-input"
name="task"
type="text"
required
placeholder="What needs to be done?"
/>
{/* Add the submit control here. */}
</div>
</form>
<section className="task-region" aria-labelledby="task-list-heading">
<h2 id="task-list-heading">Tasks</h2>
<ul id="task-list" className="task-list">
{/* Add one temporary task item here so you can inspect its structure. */}
</ul>
<p id="empty-state" className="empty-state" hidden>
No tasks yet. Add one above to get started.
</p>
</section>
<footer className="task-actions">
{/* Add the control for clearing completed tasks here. */}
</footer>
</main>
);
}
export default function App() {
return (
<main className="todo-app">
<header className="todo-header">
<h1>Today's tasks</h1>
{/* Add a short description or task count here. */}
</header>
<form id="task-form" className="task-form">
<label htmlFor="task-input">New task</label>
<div className="task-entry">
<input
id="task-input"
name="task"
type="text"
required
placeholder="What needs to be done?"
/>
{/* Add the submit control here. */}
</div>
</form>
<section className="task-region" aria-labelledby="task-list-heading">
<h2 id="task-list-heading">Tasks</h2>
<ul id="task-list" className="task-list">
{/* Add one temporary task item here so you can inspect its structure. */}
</ul>
<p id="empty-state" className="empty-state" hidden>
No tasks yet. Add one above to get started.
</p>
</section>
<footer className="task-actions">
{/* Add the control for clearing completed tasks here. */}
</footer>
</main>
);
}
Use a real <button type="submit"> inside the form. The form should still make sense before JavaScript exists: a user can enter a task and submit it, even though the submit behavior will come later.
Tasks
Check the browser:
- The page has one clear heading.
- The input has a visible label.
- Pressing
Tabreaches the input and then the submit button. - The task section has its own heading.
Tasks
2. Add one task item as a temporary interface fixture
Inside the <ul>, create one task that demonstrates the controls your later code will manage.
The checkbox represents completion. The delete button must be a button, not a link, because it performs an action without navigating.
<li className="task-item" data-task-id="replace-with-a-stable-id">
<input
className="task-checkbox"
type="checkbox"
id="replace-with-checkbox-id"
/* Connect this label to the checkbox. */
/>
<label htmlFor="replace-with-checkbox-id" className="task-label">
Replace this text with a realistic task
</label>
<button
type="button"
className="delete-task"
/* Give this button an accessible name. */
>
Delete
</button>
</li>
<li className="task-item" data-task-id="replace-with-a-stable-id">
<input
className="task-checkbox"
type="checkbox"
id="replace-with-checkbox-id"
/* Connect this label to the checkbox. */
/>
<label htmlFor="replace-with-checkbox-id" className="task-label">
Replace this text with a realistic task
</label>
<button
type="button"
className="delete-task"
/* Give this button an accessible name. */
>
Delete
</button>
</li>
Tasks
Adapt the IDs so they are unique, and replace the placeholder task text. Keep the data-task-id attribute: it gives later JavaScript a reliable way to identify the task without depending on its visible wording.
Add a submit button and the completed-task action in the places marked earlier. Give each action a clear visible label.
Tasks
Check the browser again:
- You can click the task text to toggle its checkbox.
- The delete control does not submit the form.
- Every action is reachable with the keyboard.
- The task item reads sensibly if you inspect it without the CSS.
3. Make the empty state part of the same interface
The empty state should already exist in the DOM even while the temporary task is visible. Later JavaScript can remove or hide task items and reveal this message when the list has no tasks.
Tasks
For now, leave hidden on the empty-state paragraph. Then use your browser’s inspector to remove the hidden attribute temporarily.
The result should be a useful message rather than a blank list:
No tasks yet. Add one above to get started.
Restore hidden afterward. This checks that both list states are represented by your structure:
- tasks exist → the list is visible
- no tasks exist → the empty state can be revealed
Tasks
4. Add only enough CSS to see the regions clearly
Do not build the final visual design yet. Add spacing and a small amount of layout so you can verify the structure.
.todo-app {
/* Set a readable width and center the interface. */
}
.todo-header,
.task-form,
.task-region,
.task-actions {
/* Add consistent spacing between the main regions. */
}
.task-entry {
/* Keep the input and submit button aligned. */
}
.task-list {
/* Remove the default list styling and add vertical spacing. */
}
.task-item {
/* Lay out the checkbox, label, and delete button. */
}
.empty-state[hidden] {
display: none;
}
.todo-app {
/* Set a readable width and center the interface. */
}
.todo-header,
.task-form,
.task-region,
.task-actions {
/* Add consistent spacing between the main regions. */
}
.task-entry {
/* Keep the input and submit button aligned. */
}
.task-list {
/* Remove the default list styling and add vertical spacing. */
}
.task-item {
/* Lay out the checkbox, label, and delete button. */
}
.empty-state[hidden] {
display: none;
}
Keep the empty-state rule even if your browser already hides the hidden attribute. It makes the intended behavior explicit.
Tasks
Check the page at a narrow browser width:
- The input and button remain usable.
- The task label has room to wrap.
- The delete button remains visually separate from the task text.
- The empty state is not visible while the temporary task exists.
Tasks
5. Inspect the finished interface
Use the browser inspector and confirm that your page contains this small set of targets:
#task-form#task-input#task-list#empty-state- one task item with
data-task-id - a checkbox for completion
- a delete button
- a control for clearing completed tasks
Tasks
Then submit the form once with an empty input. The browser should prevent submission because the input is required.
Finally, remove the temporary task item from the rendered page only if your component currently needs to start empty. Keep its structure nearby in your code or notes so the later JavaScript lesson can recreate the same shape.
Course Outline
3 modules · 9 lessons
Shape the standalone to-do page
Add the smallest Express server
Connect the browser to task actions
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.