Topic 17 of 56 · Full Stack Advanced

Topic 2 : Node Projects

Lesson TL;DRTopic 2: Node Projects 📖 8 min read · 🎯 beginner · 🧭 Prerequisites: insertselectupdatedeletequeries, macinstallation Why this matters Up until now, you've probably thought of Node.js as just "the t...
8 min read·beginner·node-js · express · socket-io · rest-api

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 array
  • GET /books/:id — uses req.params.id to look up one book; sends 404 if missing
  • POST /books — reads req.body (enabled by express.json() middleware); responds with 201 Created
  • PUT /books/:id — mutates the found book in place
  • DELETE /books/:id — uses findIndex + splice to 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 client
  • socket.on('chat message', ...) listens for a message from that specific client
  • io.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() wraps fs.readFileSync in a try/catch — if tasks.json doesn't exist yet, it returns an empty array instead of crashing
  • saveTasks() writes the updated array back to disk with JSON.stringify
  • Each yargs.command block declares: the command name, a description, an optional builder (defines --flags with types and required status), and a handler (runs when the command is invoked)
  • demandOption: true makes a flag required — yargs will print an error and exit if it's missing
  • yargs.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:

  1. Add a done boolean field when tasks are created in the add handler (done: false)
  2. Add a new yargs.command for done that accepts --title, finds the matching task, and sets done: true
  3. Update the list handler 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 read req.body on POST/PUT requests.
  • Socket.io enables bidirectional real-time communication; io.emit broadcasts to all clients while socket.emit targets only one.
  • yargs turns 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.writeFileSync is the simplest storage layer for a CLI tool — no database required.
  • All three projects share the same npm init -ynpm install → write entry file → node <file> workflow.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

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