Topic 2 of 18 · Node Expert

Topic 2 : Node Projects

Lesson TL;DRTopic 2: Node Projects 📖 8 min read · 🎯 beginner · 🧭 Prerequisites: themodelviewcontrollerpattern, configuringexpress Why this matters Up until now, you've probably written a few Node.js lines and ...
8 min read·beginner·node-js · express · socket-io · rest-api

Topic 2: Node Projects

📖 8 min read · 🎯 beginner · 🧭 Prerequisites: the-model-view-controller-pattern, configuring-express

Why this matters

Up until now, you've probably written a few Node.js lines and run them — and that's great. But "writing some code" and "building a real Node project" are two very different things. When you sit down to actually build something — a REST API, a chat app, a command-line tool — you need to know how to structure it from the start. What files go where? How does everything connect? In this lesson, we build three real things: a RESTful API, a live chat room with Socket.io, and a CLI tool with yargs. By the end, you'll know exactly how a Node project comes together.

What You'll Learn

  • Scaffold and run a CRUD RESTful API with Express.js
  • Build a real-time chat application using Socket.io on a shared HTTP server
  • Create a CLI task manager with persistent JSON storage using yargs
  • Understand how each project type maps to a different Node.js capability

The Analogy

Think of Node.js as a Swiss Army knife sitting on a workbench. One blade is an Express HTTP server — perfect for answering requests like a librarian handing out books on demand. A second blade is Socket.io — a two-way radio that keeps all connected parties talking in real time. A third blade is a file-system-backed CLI — a sticky notepad on the bench you can write to and read from any time. Each blade is the same knife, just opened in a different direction to solve a different problem.

Chapter 1: Project 1 — Simple RESTful API

the trainer called up the first blueprint: a book registry API exposing the classic five CRUD operations over HTTP.

Step 1: Set Up the Project

mkdir books-api
cd books-api
npm init -y
npm install express

Step 2: Create the Server

server.js — the entire API lives in one file for this project:

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 details worth noting:

  • express.json() middleware parses incoming request bodies as JSON automatically.
  • The in-memory books array acts as a temporary data store — it resets on server restart.
  • Route parameter :id is always a string; parseInt(req.params.id) converts it for the array lookup.
  • Status codes follow HTTP conventions: 201 Created on POST, 404 Not Found when the book is missing.

Step 3: Run the Server

node server.js

The API is now live at http://localhost:3000. Use Postman or cURL to test each route:

MethodRouteAction
GET/booksList all books
GET/books/:idGet one book
POST/booksAdd a book
PUT/books/:idUpdate a book
DELETE/books/:idDelete a book

Chapter 2: Project 2 — Real-Time Chat with Socket.io

"Now for something that talks back in real time," said the trainer, rolling out the second blueprint.

Socket.io layers a persistent, bidirectional WebSocket channel on top of a standard Node http server. The same port serves both the HTML client and the Socket.io upgrade handshake.

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}`);
});

Architecture notes:

  • http.createServer(app) wraps the Express app so Socket.io can share the same TCP port.
  • io.on('connection', ...) fires once per new browser tab that connects.
  • socket.on('chat message', ...) listens for a message from one client.
  • io.emit('chat message', msg) broadcasts that message to all connected clients (including the sender).

Step 3: Create the Frontend

index.html — served statically from the same Node process:

<!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>

/socket.io/socket.io.js is served automatically by the Socket.io server — no separate install or CDN link needed.

Step 4: Run the Server

node server.js

Open http://localhost:3000 in two browser tabs. Type a message in one tab and watch it appear instantly in both — that is the Socket.io broadcast in action.

sequenceDiagram
    participant ClientA as Browser A
    participant Server as Node + Socket.io
    participant ClientB as Browser B

    ClientA->>Server: socket.emit('chat message', 'Hello!')
    Server->>ClientA: io.emit('chat message', 'Hello!')
    Server->>ClientB: io.emit('chat message', 'Hello!')

Chapter 3: Project 3 — Task Management CLI Tool

"The third project never opens a browser at all," the trainer said with a grin. "Pure command line."

This tool stores tasks in a local tasks.json file and exposes three subcommands — add, list, and remove — via the yargs library.

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:

  • yargs.command({ command, describe, builder, handler }) — each call registers one subcommand.
  • builder defines named flags; demandOption: true makes a flag required.
  • argv inside handler holds the parsed flag values.
  • loadTasks wraps the file read in a try/catch so a missing tasks.json silently returns an empty array.
  • saveTasks overwrites the file on every mutation — simple but sufficient for a local CLI.

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:

(The final list prints nothing because the task was removed and the file is now an empty array.)

🧪 Try It Yourself

Extend the books API to persist data to disk.

Right now books lives in memory and resets every time you restart server.js. Replace it with file-based persistence the same way task-cli does it.

Task: In the books-api project, add two helper functions — loadBooks() and saveBooks(books) — that read from and write to a books.json file. Wire them into every route handler so the data survives a server restart.

Starter snippet:

const fs = require('fs');
const BOOKS_FILE = 'books.json';

const loadBooks = () => {
    try {
        return JSON.parse(fs.readFileSync(BOOKS_FILE).toString());
    } catch (e) {
        return [];
    }
};

const saveBooks = (books) => {
    fs.writeFileSync(BOOKS_FILE, JSON.stringify(books, null, 2));
};

Success criterion: Stop the server with Ctrl+C, restart it with node server.js, then GET /books and see the books you previously POSTed still present in the response.

🔍 Checkpoint Quiz

Q1. Why does the chat application use http.createServer(app) instead of calling app.listen() directly?

A) Express requires it when using middleware B) Socket.io needs a raw http.Server instance to attach its WebSocket layer to C) app.listen() is deprecated in newer versions of Express D) http.createServer is faster than app.listen

Q2. Given this snippet from task.js:

const loadTasks = () => {
    try {
        const dataBuffer = fs.readFileSync(tasksFile);
        return JSON.parse(dataBuffer.toString());
    } catch (e) {
        return [];
    }
};

What does this function return when tasks.json does not yet exist?

A) null B) undefined C) [] D) It throws a FileNotFoundError

Q3. In the books API, a POST /books request arrives with the body { "title": "Dune", "author": "Frank Herbert" }. What HTTP status code does the server send back, and why?

A) 200 — the default success code B) 201 — signalling that a new resource was created C) 204 — no content, creation was silent D) 202 — the request was accepted for processing

Q4. You want to add a complete command to task-cli that marks a task done without deleting it. Which yargs builder property would you use to make the --title flag required for that command?

A) required: true B) optional: false C) demandOption: true D) mandatory: true

A1. B — Socket.io attaches its WebSocket upgrade listener directly to the http.Server instance. app.listen() creates an internal server you can't reference, so there is no object to pass to socketIo().

A2. C — fs.readFileSync throws when the file is missing; the catch block returns [], giving callers a safe empty array to push into.

A3. B — HTTP 201 Created is the correct status for a successful POST that produces a new resource. Using 200 would not be wrong but is less precise.

A4. C — demandOption: true is the yargs property that makes a flag mandatory. The others (required, optional, mandatory) are not valid yargs builder keys.

🪞 Recap

  • A RESTful API with Express maps HTTP verbs (GET, POST, PUT, DELETE) to route handlers that read and mutate an in-memory (or file-backed) data store.
  • Socket.io enables real-time bidirectional communication by sharing a raw http.Server with Express; io.emit broadcasts to all connected clients simultaneously.
  • yargs turns a Node.js script into a structured CLI with subcommands, typed flags, and built-in --help output — backed by plain fs reads and writes for persistence.
  • All three projects start with the same two steps: npm init -y and npm install <dependencies>.
  • The in-memory books array and tasks.json file are entry-level storage patterns; real applications replace them with a database, but the route and command structure stays the same.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

Block your seat for ₹2,500 and join the next cohort.