🛠Core Modules & Patterns

Get comfortable with Node.js core modules and essential patterns used in real-world apps.

📞 Working with HTTP

Working with HTTP in Node.js is essential for creating web servers and handling client requests. In this chapter, we'll explore how to create HTTP servers, handle routes manually, and understand key concepts like headers, status codes, and streaming responses.

💡 Creating an HTTP Server

To create an HTTP server in Node.js, we'll use the built-in http module. Here's how to set up a basic server:

const http = require('http');

const server = http.createServer((req, res) => {
  // Handle requests here
  res.end('Hello, World!');
});

server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Understanding HTTP Routes

Routes determine how your server responds to different URLs and HTTP methods. Here's an example of handling different routes:

const http = require('http');

const server = http.createServer((req, res) => {
  switch (req.url) {
    case '/':
      res.end('Welcome to the homepage!');
      break;
    case '/about':
      res.end('About us page');
      break;
    default:
      res.writeHead(404);
      res.end('Page not found');
  }
});

💡 HTTP Headers and Status Codes

Headers provide additional information about the request or response. Common status codes include:

  • 200: Success
  • 400: Bad Request
  • 401: Unauthorized
  • 404: Not Found
  • 500: Internal Server Error
res.writeHead(200, {
  'Content-Type': 'text/plain',
  'X-Custom-Header': 'Example'
});

res.end('Response with headers');

Streaming Responses

Streaming allows you to send data in chunks, which is useful for large files or real-time applications. Here's an example:

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  
  const chunks = ['Hello', ' ', 'World!'];
  
  chunks.forEach(chunk => {
    res.write(chunk);
  });
  
  res.end();
});

💡 Best Practices for HTTP Servers

  • Always handle error cases properly
  • Use middleware for common tasks
  • Organize routes in separate files
  • Implement proper security measures
  • Monitor server performance

🧮 fs, path, and os Modules

Welcome to Node.js core modules! In this chapter, we'll explore three essential modules: fs (file system), path (path handling), and os (operating system). These modules provide powerful tools for file manipulation, path resolution, and platform-specific operations.

💡 Key Concepts

  • fs module: Enables file system operations like reading, writing, and deleting files.
  • path module: Provides utilities for working with file paths across different operating systems.
  • os module: Offers information about the operating system and platform-specific functionality.

File System (fs) Module

The fs module allows you to interact with the file system. Here are some common operations:

  • Reading files asynchronously: fs.promises.readFile(path, options)
  • Writing files: fs.writeFileSync(path, data, options)
  • Checking if a file exists: fs.existsSync(path)
  • Creating directories: fs.mkdirSync(path, { recursive: true })
const fs = require('fs');

// Read a file
try {
  const data = await fs.promises.readFile('./example.txt', 'utf8');
  console.log(data);
} catch (err) {
  console.error('Error reading file:', err);
}

Path Module

The path module helps work with file paths in a platform-independent way. Key methods include:

  • Combining path segments: path.join(...paths)
  • Getting the directory name: path.dirname(path)
  • Extracting file extension: path.extname(path)
  • Normalizing paths: path.normalize(path)
const path = require('path');

const filePath = './examples/file.txt';
console.log('Base name:', path.basename(filePath)); // 'file.txt'
console.log('Directory:', path.dirname(filePath)); // './examples'
console.log('Extension:', path.extname(filePath)); // '.txt'

OS Module

The os module provides information about the operating system and offers platform-specific functionality.

  • Getting OS type: os.type() (e.g., 'Linux', 'Windows_NT')
  • Checking CPU architecture: os.arch()
  • Retrieving environment variables: os.env object
const os = require('os');

console.log('OS Type:', os.type());
console.log('Architecture:', os.arch());
console.log('Platform:', os.platform());

💡 Best Practices

  • Use fs.promises for asynchronous file operations to avoid callback hell.
  • Always handle errors when working with the filesystem.
  • Test path operations on different platforms to ensure compatibility.
  • Avoid hardcoding paths; use platform-independent path handling.

💡 Real-World Applications

These core modules are essential in real-world applications like file managers, static site generators, and platform-specific tools. For example:

  • Building a file backup system using fs module.
  • Creating cross-platform file organizers with path utilities.
  • Developing OS-specific configuration tools using os module.

⌛ Timers, Streams, and Buffers

💡 Timers: setTimeout and setInterval

Timers are essential for scheduling tasks in Node.js. They allow you to execute code after a specified delay (setTimeout) or repeatedly at regular intervals (setInterval). These timers work similarly to their browser counterparts but are optimized for server-side use.

// setTimeout example
const delayedMessage = () => {
  console.log('This message appears after 2 seconds!');
};
setTimeout(delayedMessage, 2000);

// setInterval example
const repeatedMessage = () => {
  console.log('This message repeats every 3 seconds!');
};
setInterval(repeatedMessage, 3000);

💡 When to Use Timers

  • When you need to delay execution of certain code
  • For scheduling periodic tasks (e.g., cleanup operations)
  • Implementing timeouts for asynchronous operations
  • Creating animations or real-time updates

Common Mistakes with Timers

  • Not clearing timers with clearTimeout or clearInterval, leading to memory leaks
  • Using synchronous operations inside timers which can block the event loop
  • Relying on exact timing precision (timers are approximate)
  • Overloading the timer queue with too many intervals

💡 Best Practices for Timer Usage

  • Always store timer IDs to clear them later
  • Use clearTimeout or clearInterval as soon as they are no longer needed
  • Keep timer callback functions lightweight
  • Avoid nested timers unless absolutely necessary
  • Consider using async/await with setTimeout for cleaner code

💡 Buffers and Streams in Node.js

Node.js handles I/O operations efficiently using Buffers and Streams. Buffers are used to store binary data while Streams allow for continuous data flow, making them ideal for handling large files or network communication.

// Reading a file with streams
const fs = require('fs');
const readStream = fs.createReadStream('input.txt', { encoding: 'utf8' });

readStream.on('data', (chunk) => {
  console.log('Received chunk:', chunk);
});

readStream.on('end', () => {
  console.log('End of file reached');
});

💡 Understanding Buffers

  • Buffers are memory-efficient representations of binary data
  • They can be created from various sources (files, network, etc.)
  • Buffer operations are non-blocking and suitable for high-performance tasks
  • Use Buffer.concat() to combine multiple buffers
// Creating a buffer
const buf = Buffer.alloc(10);
buf.write('Hello');
console.log(buf.toString()); // 'Hello\u0000\u0000\u0000\u0000'

💡 Stream Types and Their Uses

  • Readable Streams: For reading data (e.g., from files or network)
  • Writable Streams: For writing data (e.g., to files or network)
  • Duplex Streams: Can both read and write data
  • Transform Streams: Modify data as it's being passed through

💡 Timers, Buffers, and Streams Together

Combining timers with streams and buffers allows you to handle asynchronous operations efficiently. For example, you could use a timer to schedule periodic file reads or buffer processing.

const fs = require('fs');

// Read file every 5 seconds
function readLogFile() {
  const readStream = fs.createReadStream('log.txt', { encoding: 'utf8' });
  
  readStream.on('data', (chunk) => {
    console.log('Log data:', chunk);
  });
}

// Schedule log reading
const logInterval = setInterval(readLogFile, 5000);

// Clear interval after 1 minute
setTimeout(() => {
  clearInterval(logInterval);
  console.log('Stopped monitoring log file');
}, 60000);

🕵️‍♂️ Error Handling & Debugging

Error handling is an essential part of building robust and reliable Node.js applications. In this chapter, we'll explore various methods for catching and managing errors, as well as tools for debugging your code.

💡 Synchronous Error Handling

For synchronous operations, Node.js provides the try/catch pattern similar to other programming languages.

try {
  // Code that might throw an error
  const result = someFunction();
} catch (error) {
  // Handle the error
  console.error('An error occurred:', error);
}

Common Mistakes to Avoid

  • Not handling errors at all: `someFunction() // Potential crash point`
  • Broad try/catch blocks that catch all errors, making debugging harder.

💡 Asynchronous Error Handling

For asynchronous operations like callbacks or promises, Node.js provides additional patterns for error handling.

💡 Callbacks

fs.readFile('file.txt', (err, data) => {
  if (err) {
    // Handle error
    console.error('Error reading file:', err);
    return;
  }
  // Process the data
});

💡 Promises & Async/Await

const readFileAsync = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('file.txt', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

async function example() {
  try {
    const data = await readFileAsync();
    console.log('File read successfully:', data);
  } catch (error) {
    console.error('Error in async operation:', error);
  }
}

💡 Debugging Tools & Best Practices

When errors occur, it's important to have the right tools and techniques to debug them effectively.

  • Use `console.log()` strategically to track program flow and variable states.
  • Utilize Node.js built-in debugging with `node --inspect` for interactive sessions.
  • Consider third-party tools like VS Code Debugger or Nodemon.
const processDebugger = require('process').debug;

// Example usage:
try {
  // Code that might fail
} catch (error) {
  console.error({
    error: error.message,
    stack: error.stack,
    context: {
      someVariable: variableValue,
      processDebug: processDebugger()
    }
  });
}

💡 Key Information

  • Always include error details in logs, like `error.message` and `error.stack`.
  • Avoid logging sensitive information directly to the console or logs.

Common Mistakes to Avoid

  • Overusing try/catch blocks which can make code harder to follow.
  • Ignoring error handling in async operations, leading to silent failures.

Best Practices

  • Keep try/catch blocks as specific as possible.
  • Log errors with context, not just the error message.
  • Test your error handling code thoroughly.

Quiz

Question 1 of 19

Which module do we use to create HTTP servers in Node.js?

  • http
  • https
  • net
  • fs