Topic 2: Node Projects
📖 8 min read · 🎯 beginner · 🧭 Prerequisites: insert-select-update-delete-queries, mac-installation
Why this matters
Up until now, you've probably thought of Node.js as just "the thing that runs JavaScript on a server." But here's the thing — Node isn't just for one kind of project. The same runtime can power a REST API serving data to a mobile app, a real-time chat server pushing live messages, or a CLI tool you run straight from the terminal. Each of these is a completely different shape of application, and each one teaches you something the others can't. In this lesson, we'll build all three — so you actually see what Node.js is capable of.
What You'll Learn
- Build a fully functional RESTful API with Express.js supporting GET, POST, PUT, and DELETE routes
- Create a real-time chat application using Socket.io and a minimal HTML frontend
- Write a command-line task manager with persistent JSON storage using
yargs - Understand how to scaffold, install dependencies, and run each type of Node.js project
The Analogy
Think of Node.js as a versatile Swiss Army knife sitting in your toolbox. The first blade is a waiter at a restaurant — the RESTful API — taking orders (requests) from customers and returning dishes (responses) from the kitchen. The second blade is a walkie-talkie — Socket.io — where anyone on the channel hears every transmission the moment it goes out. The third blade is a sticky-note system pinned to a corkboard — the CLI tool — where you walk up, scribble a task, read the list, and peel one off when it's done. Same knife, completely different jobs.
Chapter 1: Project 1 — Simple RESTful API
the trainer called this one "the foundation." A RESTful API maps HTTP verbs (GET, POST, PUT, DELETE) to data operations on a resource — in this case, a list of books stored in memory.
Step 1: Set Up the Project
mkdir books-api
cd books-api
npm init -y
npm install express
Step 2: Create the Server
Create server.js in the project root:
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.json());
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// Get a book by ID
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// Add a new book
app.post('/books', (req, res) => {
const book = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(book);
res.status(201).json(book);
});
// Update a book
app.put('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// Delete a book
app.delete('/books/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Key points about the routes:
GET /books— returns the full arrayGET /books/:id— usesreq.params.idto look up one book; sends404if missingPOST /books— readsreq.body(enabled byexpress.json()middleware); responds with201 CreatedPUT /books/:id— mutates the found book in placeDELETE /books/:id— usesfindIndex+spliceto remove and return the deleted entry
Step 3: Run the Server
node server.js
The API is now live at http://localhost:3000. Use Postman or cURL to send requests:
# Get all books
curl http://localhost:3000/books
# Add a book
curl -X POST http://localhost:3000/books \
-H "Content-Type: application/json" \
-d '{"title":"The Hobbit","author":"J.R.R. Tolkien"}'
sequenceDiagram
participant Client
participant Express
participant Books Array
Client->>Express: GET /books
Express->>Books Array: read all
Books Array-->>Express: [{...}, {...}]
Express-->>Client: 200 JSON array
Client->>Express: POST /books {title, author}
Express->>Books Array: push new entry
Books Array-->>Express: updated array
Express-->>Client: 201 JSON new book
Chapter 2: Project 2 — Real-Time Chat Application with Socket.io
"The API answers one request at a time," the trainer said. "Socket.io flips the model — the server can push to every connected client the instant something happens."
Socket.io wraps WebSockets with a fallback layer so real-time messaging works across browsers reliably.
Step 1: Set Up the Project
mkdir chat-app
cd chat-app
npm init -y
npm install express socket.io
Step 2: Create the Server
server.js:
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket) => {
console.log('a user connected');
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
socket.on('disconnect', () => {
console.log('user disconnected');
});
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Important architecture note: Express is mounted on a raw http.createServer instance, and Socket.io wraps that same server. Both HTTP and WebSocket traffic share port 3000.
io.on('connection', ...)fires once per new clientsocket.on('chat message', ...)listens for a message from that specific clientio.emit('chat message', msg)broadcasts to all connected clients — including the sender
Step 3: Create the Frontend
index.html (place in the same directory as server.js):
<!DOCTYPE html>
<html>
<head>
<title>Chat App</title>
<style>
ul { list-style-type: none; padding: 0; }
li { padding: 8px; margin-bottom: 10px; background: #f4f4f4; }
input { padding: 10px; width: 80%; }
button { padding: 10px; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const form = document.getElementById('form');
const input = document.getElementById('input');
form.addEventListener('submit', function(e) {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
socket.on('chat message', function(msg) {
const item = document.createElement('li');
item.textContent = msg;
document.getElementById('messages').appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
});
</script>
</body>
</html>
The script tag src="/socket.io/socket.io.js" is served automatically by the Socket.io package — no manual file copy needed.
Step 4: Run the Server
node server.js
Navigate to http://localhost:3000 in two separate browser tabs. Type a message in one — it appears instantly in both.
sequenceDiagram
participant Browser A
participant Server (Socket.io)
participant Browser B
Browser A->>Server (Socket.io): emit('chat message', 'Hello!')
Server (Socket.io)->>Browser A: emit('chat message', 'Hello!')
Server (Socket.io)->>Browser B: emit('chat message', 'Hello!')
Chapter 3: Project 3 — Task Management CLI Tool
"Sometimes the right interface isn't a browser at all," the trainer noted. "A CLI tool lives in the terminal and keeps your data in a plain JSON file — fast, portable, zero UI overhead."
This project uses yargs to parse command-line arguments into structured commands with options, and Node's built-in fs module to persist tasks to disk.
Step 1: Set Up the Project
mkdir task-cli
cd task-cli
npm init -y
npm install yargs
Step 2: Create the CLI Tool
task.js:
const yargs = require('yargs');
const fs = require('fs');
const tasksFile = 'tasks.json';
// Load tasks
const loadTasks = () => {
try {
const dataBuffer = fs.readFileSync(tasksFile);
return JSON.parse(dataBuffer.toString());
} catch (e) {
return [];
}
};
// Save tasks
const saveTasks = (tasks) => {
fs.writeFileSync(tasksFile, JSON.stringify(tasks));
};
// Add task command
yargs.command({
command: 'add',
describe: 'Add a new task',
builder: {
title: {
describe: 'Task title',
demandOption: true,
type: 'string'
},
description: {
describe: 'Task description',
demandOption: true,
type: 'string'
}
},
handler(argv) {
const tasks = loadTasks();
tasks.push({ title: argv.title, description: argv.description });
saveTasks(tasks);
console.log('Task added:', argv.title);
}
});
// List tasks command
yargs.command({
command: 'list',
describe: 'List all tasks',
handler() {
const tasks = loadTasks();
console.log('Tasks:');
tasks.forEach((task, index) => {
console.log(`${index + 1}. ${task.title} - ${task.description}`);
});
}
});
// Remove task command
yargs.command({
command: 'remove',
describe: 'Remove a task',
builder: {
title: {
describe: 'Task title',
demandOption: true,
type: 'string'
}
},
handler(argv) {
let tasks = loadTasks();
tasks = tasks.filter((task) => task.title !== argv.title);
saveTasks(tasks);
console.log('Task removed:', argv.title);
}
});
yargs.parse();
How the pieces fit together:
loadTasks()wrapsfs.readFileSyncin a try/catch — iftasks.jsondoesn't exist yet, it returns an empty array instead of crashingsaveTasks()writes the updated array back to disk withJSON.stringify- Each
yargs.commandblock declares: the command name, a description, an optionalbuilder(defines--flagswith types and required status), and ahandler(runs when the command is invoked) demandOption: truemakes a flag required — yargs will print an error and exit if it's missingyargs.parse()at the bottom triggers argument processing
Step 3: Run the CLI Tool
node task.js add --title="Finish report" --description="Complete the annual report"
node task.js list
node task.js remove --title="Finish report"
node task.js list
Expected output:
Task added: Finish report
Tasks:
1. Finish report - Complete the annual report
Task removed: Finish report
Tasks:
After remove, the list is empty and tasks.json reflects that. The file persists between runs — kill and restart the process and your tasks are still there.
🧪 Try It Yourself
Task: Extend the CLI task manager to support a done command that marks a task complete rather than deleting it.
Steps:
- Add a
doneboolean field when tasks are created in theaddhandler (done: false) - Add a new
yargs.commandfordonethat accepts--title, finds the matching task, and setsdone: true - Update the
listhandler to show[x]or[ ]beside each task title
Starter snippet for the done command:
yargs.command({
command: 'done',
describe: 'Mark a task as complete',
builder: {
title: {
describe: 'Task title',
demandOption: true,
type: 'string'
}
},
handler(argv) {
const tasks = loadTasks();
const task = tasks.find(t => t.title === argv.title);
if (!task) return console.log('Task not found:', argv.title);
task.done = true;
saveTasks(tasks);
console.log('Task marked done:', argv.title);
}
});
Success criterion: Running node task.js list should print [x] Finish report for completed tasks and [ ] Buy groceries for pending ones.
🔍 Checkpoint Quiz
Q1. In the books API, why does the POST /books route respond with status 201 instead of 200?
A) Express requires it for POST routes
B) 201 Created semantically signals that a new resource was created, not just a successful operation
C) 200 is reserved for GET requests only
D) The books array throws an error if you use 200
Q2. Given this Socket.io server snippet:
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
If Browser A sends a message, which clients receive the chat message event?
A) Only Browser A (the sender)
B) All clients except Browser A
C) All connected clients, including Browser A
D) Only the most recently connected client
Q3. In task.js, the loadTasks function wraps fs.readFileSync in a try/catch and returns [] on error. What specific scenario does this handle, and what would happen without it?
Q4. You want to add a GET /books?author=Orwell query-parameter filter to the books API. Which of the following correctly reads the query parameter in Express?
A) req.params.author
B) req.body.author
C) req.query.author
D) req.headers.author
A1. B — HTTP semantics distinguish 200 OK (general success) from 201 Created (a new resource was created). Using 201 makes the API's intent explicit to any client consuming it.
A2. C — io.emit(...) broadcasts to all connected sockets, including the one that sent the message. To exclude the sender you would use socket.broadcast.emit(...) instead.
A3. If tasks.json doesn't exist yet (first run), fs.readFileSync throws a ENOENT error. The catch block silently returns [] so the first add command can proceed normally. Without it, every command would crash on a fresh install before any tasks are saved.
A4. C — Express parses URL query strings (?author=Orwell) into req.query. req.params is for route segments (:id), req.body is for request body payload, and req.headers is for HTTP headers.
🪞 Recap
- A RESTful API with Express maps HTTP verbs to CRUD operations;
express.json()middleware is required to readreq.bodyon POST/PUT requests. - Socket.io enables bidirectional real-time communication;
io.emitbroadcasts to all clients whilesocket.emittargets only one. yargsturns a Node.js script into a structured CLI with named commands, typed flags, and built-in help text.- Persisting data to a JSON file with
fs.readFileSync/fs.writeFileSyncis the simplest storage layer for a CLI tool — no database required. - All three projects share the same
npm init -y→npm install→ write entry file →node <file>workflow.
📚 Further Reading
- Express.js official docs — the source of truth for routing, middleware, and request/response APIs
- Socket.io documentation — covers rooms, namespaces, and advanced real-time patterns beyond basic
emit - yargs documentation — full reference for command builders, option types, positional args, and middleware
- Node.js
fsmodule docs — covers both sync and async file I/O used in the CLI project - ⬅️ Previous: Mac Installation
- ➡️ Next: React Structure