Node.js Fundamentals
A Complete Introduction to Server-Side JavaScript
By Philip Osztromok & Claude
Version 1.0
2026

Table of Contents

  • 1 Event Loop & Non-Blocking I/O
  • 2 Modules & npm
  • 3 Callbacks & Promises
  • 4 Async/Await
  • 5 File System Operations
  • 6 HTTP Servers
  • 7 Debugging & Error Handling
  • 8 Best Practices & Wrap-Up

Introduction

Welcome to Node.js Fundamentals!

This comprehensive course introduces you to Node.js, a powerful runtime environment that lets you build server-side applications using JavaScript. Whether you're a web developer looking to expand into backend development or a newcomer to programming, this course provides everything you need to master the fundamentals.

What You'll Learn

Throughout this course, you will understand:

  • How Node.js works under the hood (the event loop, non-blocking I/O, and the callback queue)
  • How to organize code with the module system and manage packages with npm
  • Three approaches to async programming: callbacks, promises, and async/await
  • How to read and write files, work with directories, and manage file I/O
  • How to create HTTP servers, handle requests, and build simple APIs
  • Professional debugging techniques and robust error handling strategies
  • Best practices for production-ready applications

How to Use This Course

Each chapter follows a consistent structure: a comprehensive lesson explaining core concepts, followed by three progressively challenging coding challenges with complete solutions. You can read through the lessons to build understanding, then work through the challenges to practice.

Prerequisites: Basic JavaScript knowledge (variables, functions, objects). You'll need Node.js installed on your computer.

Time Commitment: Expect 3-4 weeks of dedicated study, working through one chapter per 2-3 days.

Let's begin your journey into Node.js development!

CHAPTER 1
Event Loop & Non-Blocking I/O
Understanding how Node.js executes code asynchronously

Node.js is built on an event-driven, non-blocking I/O model. This makes it incredibly efficient for I/O-heavy operations like reading files, querying databases, or handling network requests. Understanding the event loop is crucial to mastering Node.js.

The Event Loop

The event loop is the heart of Node.js. It continuously checks for tasks to execute:

  1. Call Stack: Synchronous code executes here immediately
  2. Web APIs/Event Loop: Async operations (timers, file I/O, network requests) are handed to the system
  3. Callback Queue: When async operations complete, their callbacks wait here
  4. Event Loop: Moves callbacks from the queue to the stack when it's empty
// Example showing event loop order console.log('1. Start'); setTimeout(() => { console.log('2. Timeout (async)'); }, 0); console.log('3. End'); // Output: // 1. Start // 3. End // 2. Timeout (async)

Even though the timeout is 0ms, it doesn't execute immediately. Synchronous code (console.log calls) runs first, then the event loop processes the timeout callback.

Blocking vs Non-Blocking I/O

Blocking I/O pauses execution until the operation completes. Non-blocking I/O starts the operation and continues executing other code.

Blocking vs Non-Blocking Comparison
❌ Blocking (Slow)
const data = fs.readFileSync('file.txt'); // Waits here until file is read console.log(data);

Program pauses. If reading 3 files takes 150ms each, total = 450ms.

✅ Non-Blocking (Fast)
fs.readFile('file.txt', (err, data) => { console.log(data); }); // Continues immediately

Program continues. If reading 3 files concurrently at 150ms each, total = ~150ms.

Why Non-Blocking Matters

For a web server handling 1000 concurrent requests:

  • Blocking approach: 1000 × 50ms = 50 seconds ❌
  • Non-blocking approach: ~50ms total ✅
💡 Key Insight

Node.js shines when you have many I/O operations happening concurrently. Its event-driven model handles thousands of connections efficiently without creating a thread per connection.

What You've Learned

  • The event loop processes code in phases: call stack, async APIs, and callback queue
  • Blocking I/O pauses execution; non-blocking I/O continues immediately
  • Synchronous code always runs before async callbacks
  • Node.js efficiently handles concurrent I/O operations
CHAPTER 2
Modules & npm
Organizing code and managing packages

Node.js uses a module system to organize code. Modules are reusable pieces of code that export functionality for other files to use. npm (Node Package Manager) lets you download and manage third-party packages.

The Module System (CommonJS)

A module is just a JavaScript file. Use module.exports to share code:

// math.js function add(a, b) { return a + b; } module.exports = { add }; // app.js const math = require('./math'); console.log(math.add(5, 3)); // 8

npm and package.json

Every Node.js project has a package.json file that describes the project and its dependencies:

{ "name": "my-app", "version": "1.0.0", "dependencies": { "express": "^4.18.0", "lodash": "^4.17.21" }, "scripts": { "start": "node app.js" } }

Installing Packages

Use npm to install packages from the registry:

$ npm install express

This downloads the package to node_modules/ and updates package.json.

⚠️ Important: Never commit node_modules/ to git. Add it to .gitignore. When cloning a repo, run npm install to reinstall dependencies from package.json.

Key Concepts

module.exports

What a module shares. Can be a function, object, class, or anything.

require()

Load a module. Returns what the module exported.

Dependencies

Packages your app needs to run (installed in production).

devDependencies

Packages for development only (testing, linting).

CHAPTER 3
Callbacks & Promises
Two patterns for handling asynchronous operations

Callbacks and Promises are two ways to handle async operations. Both have their strengths, and understanding both helps you read existing code and make informed choices.

Callbacks

A callback is a function passed to another function, called when an operation completes:

function fetchUser(id, callback) { setTimeout(() => { const user = { id, name: 'Alice' }; callback(null, user); // Error-first convention }, 1000); } fetchUser(1, (err, user) => { if (err) { console.error('Error:', err); } else { console.log('User:', user); } });

The Callback Hell Problem

Deeply nested callbacks become hard to read:

getUser(1, (err, user) => { getOrders(user.id, (err, orders) => { getOrderDetails(orders[0].id, (err, details) => { // Deeply nested... hard to follow! }); }); });

Promises

A Promise represents a value that will be available in the future. It has three states:

  • Pending: Operation hasn't finished
  • Resolved: Operation succeeded with a value
  • Rejected: Operation failed with an error
function fetchUser(id) { return new Promise((resolve, reject) => { if (id > 0) { resolve({ id, name: 'Alice' }); } else { reject(new Error('Invalid ID')); } }); } fetchUser(1) .then(user => console.log('User:', user)) .catch(err => console.error('Error:', err));

Promise Chains

Chain operations with .then() for cleaner, more readable code:

fetchUser(1) .then(user => getOrders(user.id)) .then(orders => getOrderDetails(orders[0].id)) .then(details => console.log('Details:', details)) .catch(err => console.error('Error:', err));

Much cleaner than callback nesting! Each .then() receives the resolved value from the previous one.

💡 Quick Comparison

Callbacks: Simple for single operations, but nest deeply for chains. Promises: Clean chains, better error handling with single .catch().

CHAPTER 4
Async/Await
Modern syntax for writing clean asynchronous code

Async/await is syntactic sugar built on Promises. It lets you write asynchronous code that looks and feels synchronous—no more chains of .then() calls.

Async Functions

An async function always returns a Promise:

async function greet() { return 'Hello!'; } greet().then(msg => console.log(msg)); // Hello!

The await Keyword

await pauses execution until a Promise resolves. It can only be used inside an async function:

async function loadUser(id) { const user = await fetchUser(id); // Pause here console.log('User:', user); return user; }

Async/Await vs Promises

Side-by-Side Comparison
Promise Chains
fetchUser(1) .then(user => { return fetchOrders(user.id); }) .then(orders => { console.log(orders); }) .catch(err => { console.error(err); });
Async/Await
async function load() { try { const user = await fetchUser(1); const orders = await fetchOrders(user.id); console.log(orders); } catch (err) { console.error(err); } }

Error Handling with Try/Catch

Async/await uses standard try/catch for error handling:

async function loadData(id) { try { const user = await fetchUser(id); const orders = await fetchOrders(user.id); return { user, orders }; } catch (error) { console.error('Failed:', error.message); return null; } }

Concurrent Operations

For independent operations, use Promise.all() to run them in parallel:

// Sequential (slow): 2000ms const user = await fetchUser(1); // 1000ms const orders = await fetchOrders(1); // 1000ms // Concurrent (fast): ~1000ms const [user, orders] = await Promise.all([ fetchUser(1), fetchOrders(1) ]);
💡 Best Practice

Use async/await for modern Node.js code. It's cleaner, easier to debug, and easier to understand than callbacks or promise chains.

CHAPTER 5
File System Operations
Reading, writing, and managing files

The fs module lets you interact with files and directories. You can read, write, delete, and manipulate files using async operations.

Reading Files

const { promises: fs } = require('fs'); async function readTextFile(filename) { try { const content = await fs.readFile(filename, 'utf8'); console.log('Content:', content); return content; } catch (error) { console.error('Error reading file:', error.message); } }

Writing Files

async function writeFile(filename, content) { try { await fs.writeFile(filename, content, 'utf8'); console.log('✓ File written'); } catch (error) { console.error('Error writing file:', error.message); } }

Directory Operations

// List files const files = await fs.readdir('./data'); // Create directory await fs.mkdir('./output/subdir', { recursive: true }); // Get file stats const stats = await fs.stat('file.txt'); console.log('Size:', stats.size); console.log('Modified:', stats.mtime);

Appending to Files

// Append content (creates file if doesn't exist) await fs.appendFile('log.txt', 'New log entry\n');

Deleting Files

// Delete a file await fs.unlink('old-file.txt'); // Delete a directory await fs.rmdir('./empty-dir'); // Delete recursively await fs.rm('./dir-with-files', { recursive: true });
⚠️ Important: Never use synchronous file operations (readFileSync, writeFileSync) in production. They block execution and prevent your server from handling other requests.

Common Patterns

  • Process JSON: Read JSON file, parse it, modify, write back
  • Process CSV: Read file, split lines, process data
  • Copy file: Read source, write to destination
  • Generate report: Collect data, format, write to file
CHAPTER 6
HTTP Servers
Creating web servers and handling requests

The http module lets you create web servers. Clients send HTTP requests to your server, and your server sends back responses.

Creating a Basic Server

const http = require('http'); const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello, World!'); }); server.listen(3000, 'localhost', () => { console.log('Server running at http://localhost:3000/'); });

Understanding Request & Response Objects

The req object contains request information:

req.method // 'GET', 'POST', etc. req.url // '/path?query=value' req.headers // { host, user-agent, ... }

The res object sends responses:

res.statusCode = 200; // HTTP status res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data));

Routing

Handle different URLs differently:

if (req.url === '/' && req.method === 'GET') { res.end('Home page'); } else if (req.url === '/about') { res.end('About page'); } else { res.statusCode = 404; res.end('Not found'); }

Sending JSON

const data = { message: 'Hello', count: 42 }; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data, null, 2));

Handling POST Requests

if (req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { const data = JSON.parse(body); console.log('Received:', data); res.end(JSON.stringify({ success: true })); }); }

HTTP Status Codes

  • 200: OK (success)
  • 201: Created (resource created)
  • 400: Bad Request (invalid input)
  • 404: Not Found
  • 500: Internal Server Error
💡 Next Step

For production apps, use Express.js instead of raw http module. Express provides routing, middleware, and many other conveniences.

CHAPTER 7
Debugging & Error Handling
Finding and fixing bugs, handling errors gracefully

Professional error handling and debugging are essential for production apps. You'll spend as much time debugging as coding—better tools and patterns make this easier.

Error Types

  • TypeError: Wrong type (calling non-function)
  • ReferenceError: Undefined variable
  • SyntaxError: Invalid syntax
  • RangeError: Value out of range

Try/Catch Blocks

try { const result = riskyOperation(); console.log('Success:', result); } catch (error) { console.error('Error:', error.message); } finally { cleanup(); // Always runs }

Custom Error Classes

Create meaningful errors for your domain:

class ValidationError extends Error { constructor(message, field) { super(message); this.name = 'ValidationError'; this.field = field; this.statusCode = 400; } } throw new ValidationError('Email required', 'email');

Logging

Use appropriate log levels:

console.debug('Detailed debugging info'); console.log('General information'); console.warn('Warning (recoverable)'); console.error('Error (needs attention)');

Error Handling in Async Code

async function loadData() { try { const data = await fetchData(); return data; } catch (error) { console.error('Failed:', error.message); throw error; // Re-throw for caller } }

Debugging Tools

  • console methods: log, warn, error, table, time
  • Node debugger: node inspect app.js
  • Chrome DevTools: chrome://inspect
  • VS Code debugger: Built-in debugging

Stack Traces

Stack traces show the call chain when an error occurs. Read them from bottom to top to understand the sequence of function calls that led to the error.

💡 Rule of Thumb

Always handle errors. Never swallow them silently. Log, re-throw, or return a fallback value—but do something.

CHAPTER 8
Best Practices & Wrap-Up
Industry standards and next steps

This final chapter reinforces best practices and covers what you need to know to deploy production applications.

Code Quality

  • Use meaningful variable names
  • Keep functions small and focused (single responsibility)
  • Avoid deep nesting (max 3 levels)
  • DRY: Don't Repeat Yourself
  • Use a linter (ESLint) to enforce consistency

Async Best Practices

  • Prefer async/await over callbacks and promises
  • Use Promise.all() for independent concurrent operations
  • Set timeouts for external API calls
  • Implement retry logic for transient failures

Security Basics

  • Input Validation: Validate all user input
  • Environment Variables: No hardcoded secrets
  • HTTPS: Always use TLS in production
  • No Command Injection: Never execute user input as commands
  • Error Messages: Don't expose stack traces to users

Performance Optimization

  • No synchronous file/database operations
  • Cache computed results when beneficial
  • Use streams for large files (don't load into memory)
  • Run independent operations concurrently
  • Monitor and optimize database queries

Testing

Always test your code! Common frameworks:

  • Jest: Full-featured testing
  • Mocha: Flexible test framework
  • Vitest: Fast unit tests

Course Summary

You've learned:

  • How Node.js event loop and non-blocking I/O work
  • How to organize code with modules and npm
  • Three async patterns: callbacks, promises, async/await
  • File system operations and HTTP servers
  • Professional debugging and error handling
  • Best practices for production applications

Next Steps

Where to go from here:

  • Express.js: Learn the popular web framework (our next course)
  • Databases: Learn MongoDB, PostgreSQL, or MySQL
  • Real Projects: Build something useful and deploy it
  • Testing: Master unit and integration testing
  • DevOps: Learn Docker and deployment

Congratulations!

You now have a solid foundation in Node.js. You understand how to build efficient, scalable server-side applications with JavaScript. The next step is to build something—learning by doing is the most effective way to master these skills.

Happy coding! 🚀

Appendix: Resources & Further Learning

Official Documentation

  • Node.js Official Docs: nodejs.org/docs
  • Node.js API Reference: nodejs.org/api
  • npm Registry: npmjs.com

Recommended Frameworks & Tools

  • Express.js — Web application framework
  • Jest — Testing framework
  • ESLint — Code linter
  • PM2 — Process manager
  • Nodemon — Auto-reload during development

Learning Resources

  • FreeCodeCamp — Comprehensive tutorials
  • GitHub — Study open-source Node.js projects
  • Stack Overflow — Search for answers to your questions
  • Dev.to — Articles by Node.js developers

Hosting Platforms

  • Heroku — Easy deployment, great for beginners
  • AWS — Maximum control and scale
  • DigitalOcean — Simple and affordable
  • Google Cloud — Excellent tools and documentation

Key Takeaways

  • Understand the event loop—it's the heart of Node.js
  • Always use async/await for modern code
  • Handle errors gracefully and log appropriately
  • Test your code; bugs in production are expensive
  • Security matters—validate input, use environment variables, never hardcode secrets
  • Monitor your applications in production
  • Keep learning—JavaScript and Node.js evolve constantly

Thank you for working through this course. You're now equipped with the knowledge to build production-ready Node.js applications. Good luck with your projects!