CPCODELAB
Practice

Practice Node.js

You’ve read the lessons — now prove it. Take the quiz for instant feedback, then work the coding exercises. Try each one yourself before you reveal the solution.

Quick quiz

17 questions
Score: 0 / 17
0/17 answered
  1. 1

    Which function is used to load a module in CommonJS (CJS) Node.js?

  2. 2

    What does module.exports refer to in a CommonJS module?

  3. 3

    Which field in package.json defines the entry point for a CommonJS package?

  4. 4

    What is the correct way to install a package as a dev dependency?

  5. 5

    Which built-in Node.js module provides file-system operations such as reading and writing files?

  6. 6

    Given the code below, what is printed first? ``js console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');

  7. 7

    In the Node.js event loop, which queue is processed before setTimeout callbacks?

  8. 8

    How do you emit a custom event called 'data' on an EventEmitter instance emitter?

  9. 9

    Which of the following correctly reads the value of an environment variable called PORT?

  10. 10

    What does fs.createReadStream() return?

  11. 11

    Which http module method creates an HTTP server that listens for incoming requests?

  12. 12

    Which class must you extend to create a custom Transform stream in Node.js?

  13. 13

    What module do you import to create a Worker thread in Node.js?

  14. 14

    What does process.nextTick(fn) guarantee about when fn is called?

  15. 15

    Which os module method returns the number of logical CPU cores available on the machine?

  16. 16

    In Node.js, when both setTimeout(fn, 0) and setImmediate(fn) are scheduled inside an I/O callback, which fires first?

  17. 17

    What is the correct method to safely create a new Buffer of n zero-filled bytes in modern Node.js?

🛠️

Coding exercises

8 tasks
1

Hello, Module!

Easy

Create a CommonJS module that exports a function greet(name) returning the string "Hello, <name>!". Then, in the same file (after the export), call greet with your own name and log the result to the console.

Starter
js
// greet.js
function greet(name) {
  // TODO: return the greeting string
}

module.exports = { greet };

// Call greet here and log the result
💡 Show hint

Use a template literal or string concatenation to build the greeting, then use console.log() to print it.

✅ Show solution
js
// greet.js
function greet(name) {
  return `Hello, ${name}!`;
}

module.exports = { greet };

// Quick smoke-test
const { greet: g } = module.exports;
console.log(g("Alice")); // Hello, Alice!
2

Read a File with `fs`

Easy

Write a Node.js script that: 1. Uses fs.readFile to asynchronously read a file called notes.txt in the same directory. 2. If an error occurs (e.g. the file does not exist), logs "Error: " + err.message. 3. On success, logs the file contents as a UTF-8 string.

Starter
js
const fs = require("fs");
const path = require("path");

const filePath = path.join(__dirname, "notes.txt");

// TODO: read the file asynchronously
💡 Show hint

Pass "utf8" as the second argument to fs.readFile so the callback receives a string instead of a Buffer.

✅ Show solution
js
const fs = require("fs");
const path = require("path");

const filePath = path.join(__dirname, "notes.txt");

fs.readFile(filePath, "utf8", (err, data) => {
  if (err) {
    console.log("Error: " + err.message);
    return;
  }
  console.log(data);
});
3

Simple HTTP Server

Medium

Build an HTTP server using Node.js's built-in http module that: - Listens on port 3000. - Responds to any request with status 200, Content-Type: text/plain, and the body "Hello from Node.js!". - Logs "Server running on http://localhost:3000" once it starts. Do not use Express or any third-party package.

Starter
js
const http = require("http");

const PORT = 3000;

const server = http.createServer((req, res) => {
  // TODO: send the response
});

server.listen(PORT, () => {
  // TODO: log the startup message
});
💡 Show hint

Use res.writeHead(200, { "Content-Type": "text/plain" }) then res.end("Hello from Node.js!") inside the request callback.

✅ Show solution
js
const http = require("http");

const PORT = 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello from Node.js!");
});

server.listen(PORT, () => {
  console.log("Server running on http://localhost:3000");
});
4

Custom EventEmitter — Task Queue

Medium

Using Node.js's built-in events module: 1. Create a class TaskQueue that extends EventEmitter. 2. Add a method addTask(name) that emits a "task:added" event with the task name. 3. Add a method completeTask(name) that emits a "task:done" event with the task name. 4. Instantiate TaskQueue, register listeners that log "Added: <name>" and "Done: <name>", then add and complete two tasks.

Starter
js
const { EventEmitter } = require("events");

class TaskQueue extends EventEmitter {
  addTask(name) {
    // TODO: emit "task:added"
  }

  completeTask(name) {
    // TODO: emit "task:done"
  }
}

const queue = new TaskQueue();

// TODO: register listeners and call addTask / completeTask
💡 Show hint

Inside addTask, call this.emit("task:added", name). Register listeners with queue.on("task:added", (name) => { ... }).

✅ Show solution
js
const { EventEmitter } = require("events");

class TaskQueue extends EventEmitter {
  addTask(name) {
    this.emit("task:added", name);
  }

  completeTask(name) {
    this.emit("task:done", name);
  }
}

const queue = new TaskQueue();

queue.on("task:added", (name) => {
  console.log("Added: " + name);
});

queue.on("task:done", (name) => {
  console.log("Done: " + name);
});

queue.addTask("Write tests");
queue.addTask("Deploy app");
queue.completeTask("Write tests");
queue.completeTask("Deploy app");
5

Async File Stats with `async`/`await`

Hard

Write an async Node.js CLI tool that: 1. Reads a directory path from process.argv[2] (default to "." if not provided). 2. Uses fs.promises.readdir to list all entries in the directory. 3. For each entry, uses fs.promises.stat to determine whether it is a file or directory. 4. Prints a summary table to the console in this format (one entry per line): [FILE] notes.txt or [DIR] src 5. At the end, prints "Total: X file(s), Y dir(s)". 6. Wraps everything in a try/catch and logs a user-friendly error message if anything fails.

Starter
js
const fs = require("fs").promises;
const path = require("path");

async function summarise(dirPath) {
  // TODO: list entries, stat each one, print summary
}

const target = process.argv[2] || ".";
summarise(target);
💡 Show hint

Use Promise.all(entries.map(...)) so all stat calls run concurrently instead of one-by-one. Check stat.isDirectory() to decide the label.

✅ Show solution
js
const fs = require("fs").promises;
const path = require("path");

async function summarise(dirPath) {
  try {
    const entries = await fs.readdir(dirPath);

    const stats = await Promise.all(
      entries.map(async (entry) => {
        const fullPath = path.join(dirPath, entry);
        const stat = await fs.stat(fullPath);
        return { name: entry, isDir: stat.isDirectory() };
      })
    );

    let files = 0;
    let dirs = 0;

    for (const { name, isDir } of stats) {
      if (isDir) {
        console.log("[DIR]  " + name);
        dirs++;
      } else {
        console.log("[FILE] " + name);
        files++;
      }
    }

    console.log(`\nTotal: ${files} file(s), ${dirs} dir(s)`);
  } catch (err) {
    console.error("Could not read directory: " + err.message);
  }
}

const target = process.argv[2] || ".";
summarise(target);
6

Custom Transform Stream — Line Counter

Medium

Using Node.js's built-in stream module: 1. Create a Transform stream called LineCounter that counts newline characters as data flows through it. 2. The stream should pass the original data downstream unchanged. 3. After the stream finishes, log "Total lines: <n>" using the finish event. 4. Pipe process.stdin through your LineCounter to a writable process.stdout. Run it with: node solution.js < somefile.txt

Starter
js
const { Transform } = require("stream");

class LineCounter extends Transform {
  constructor() {
    super();
    this.count = 0;
  }

  _transform(chunk, encoding, callback) {
    // TODO: count newlines and pass chunk through
  }

  _flush(callback) {
    // TODO: log the total line count
    callback();
  }
}

const counter = new LineCounter();
process.stdin.pipe(counter).pipe(process.stdout);
💡 Show hint

Inside _transform, use chunk.toString().split("\n").length - 1 to count newlines in the chunk, then call callback(null, chunk) to pass data through. Use _flush to log the final count.

✅ Show solution
js
const { Transform } = require("stream");

class LineCounter extends Transform {
  constructor() {
    super();
    this.count = 0;
  }

  _transform(chunk, encoding, callback) {
    const text = chunk.toString();
    const newlines = (text.match(/\n/g) || []).length;
    this.count += newlines;
    callback(null, chunk);
  }

  _flush(callback) {
    process.stderr.write("Total lines: " + this.count + "\n");
    callback();
  }
}

const counter = new LineCounter();
process.stdin.pipe(counter).pipe(process.stdout);
7

Worker Thread — Parallel Fibonacci

Hard

Using the worker_threads module, compute Fibonacci numbers in parallel: 1. In the main thread, create an array of inputs [35, 36, 37, 38, 39]. 2. Spawn one Worker per input, each computing the naive recursive Fibonacci for its value. 3. Collect all results with Promise.all and log them in input order (not completion order). 4. Log "All done in <ms>ms" when finished. Put the worker logic in the same file using the isMainThread flag.

Starter
js
const { Worker, isMainThread, workerData, parentPort } = require("worker_threads");

if (isMainThread) {
  const inputs = [35, 36, 37, 38, 39];

  function runWorker(n) {
    // TODO: return a Promise that resolves with the fibonacci result
  }

  async function main() {
    // TODO: run all workers in parallel and log results
  }

  main();
} else {
  // TODO: compute fibonacci for workerData.n and post the result
}
💡 Show hint

Use new Worker(__filename, { workerData: { n } }) to spawn each worker pointing at the same file. In the worker branch, implement a simple recursive fib(n) function and call parentPort.postMessage(fib(workerData.n)).

✅ Show solution
js
const { Worker, isMainThread, workerData, parentPort } = require("worker_threads");

function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

if (isMainThread) {
  const inputs = [35, 36, 37, 38, 39];

  function runWorker(n) {
    return new Promise((resolve, reject) => {
      const w = new Worker(__filename, { workerData: { n } });
      w.on("message", resolve);
      w.on("error", reject);
      w.on("exit", (code) => {
        if (code !== 0) reject(new Error("Worker exited with code " + code));
      });
    });
  }

  async function main() {
    const start = Date.now();
    const results = await Promise.all(inputs.map(runWorker));
    inputs.forEach((n, i) => console.log("fib(" + n + ") = " + results[i]));
    console.log("All done in " + (Date.now() - start) + "ms");
  }

  main();
} else {
  parentPort.postMessage(fib(workerData.n));
}
8

Graceful Shutdown with SIGTERM

Hard

Build a Node.js HTTP server that shuts down gracefully: 1. Create an http.Server listening on port 4000. 2. On every request, respond with 200 OK and the body "Hello!". 3. Listen for SIGTERM — when received, stop accepting new connections with server.close(), then log "Server closed" and call process.exit(0) inside the close callback. 4. Also listen for unhandledRejection — log "Unhandled rejection:" + the reason, then call process.exit(1). Test graceful shutdown with: kill -SIGTERM <pid>

Starter
js
const http = require("http");

const server = http.createServer((req, res) => {
  // TODO: respond with 200 and body "Hello!"
});

server.listen(4000, () => {
  console.log("Listening on http://localhost:4000");
});

// TODO: handle SIGTERM

// TODO: handle unhandledRejection
💡 Show hint

Use process.on("SIGTERM", () => { server.close(() => { ... }); }). Inside the server.close callback is the earliest moment you know all in-flight requests have finished.

✅ Show solution
js
const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello!");
});

server.listen(4000, () => {
  console.log("Listening on http://localhost:4000");
});

process.on("SIGTERM", () => {
  console.log("SIGTERM received — closing server");
  server.close(() => {
    console.log("Server closed");
    process.exit(0);
  });
});

process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
  process.exit(1);
});