Asher Cohen
Back to posts

Node.js: Server-Side JavaScript Essentials

Complete guide to Node.js development — setup, file system, streams, events, and best practices

Introduction

Node.js is a JavaScript runtime built on Chrome's V8 engine that lets you run JavaScript on the server. It's event-driven, non-blocking, and perfect for building scalable network applications, APIs, and tools.

Getting Started

Installation

# Check if Node.js is installed
node --version
npm --version

# Install via nvm (recommended)
nvm install 22
nvm use 22

Your First Server

import http from 'node:http';

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello from Node.js!' }));
});

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

Core Modules

File System (fs)

import fs from 'node:fs';
import fsPromises from 'node:fs/promises';

// Synchronous (blocking — avoid in servers)
const data = fs.readFileSync('file.txt', 'utf8');

// Callback-based
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// Promise-based (preferred)
async function readFile() {
  try {
    const data = await fsPromises.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (error) {
    console.error('Error reading file:', error);
  }
}

// Write files
await fsPromises.writeFile('output.txt', 'Hello World');

// Append to files
await fsPromises.appendFile('log.txt', 'New log entry\n');

// Check existence
import { existsSync } from 'node:fs';
if (existsSync('file.txt')) {
  // File exists
}

// Directory operations
await fsPromises.mkdir('new-folder', { recursive: true });
const files = await fsPromises.readdir('folder');

Path

import path from 'node:path';

// Join paths safely
const fullPath = path.join('users', 'alice', 'documents', 'file.txt');
// 'users/alice/documents/file.txt' (or with backslashes on Windows)

// Get file extension
path.extname('file.txt'); // '.txt'

// Get directory name
path.dirname('/users/alice/file.txt'); // '/users/alice'

// Get file name
path.basename('/users/alice/file.txt'); // 'file.txt'

// Resolve relative paths
path.resolve('src', 'index.js'); // Absolute path

// Parse path
path.parse('/users/alice/file.txt');
// { root: '/', dir: '/users/alice', base: 'file.txt', ext: '.txt', name: 'file' }

Events

import { EventEmitter } from 'node:events';

class MyEmitter extends EventEmitter {}

const emitter = new MyEmitter();

// Register event listener
emitter.on('data', (payload) => {
  console.log('Data received:', payload);
});

// One-time listener
emitter.once('connect', () => {
  console.log('Connected (only once)');
});

// Emit event
emitter.emit('data', { id: 1, value: 'test' });

// Remove listener
const handler = (data) => console.log(data);
emitter.on('event', handler);
emitter.off('event', handler);

// Error handling
emitter.on('error', (error) => {
  console.error('Error:', error);
});

Streams

import fs from 'node:fs';
import { Transform, pipeline } from 'node:stream';
import { promisify } from 'node:util';

const pipelineAsync = promisify(pipeline);

// Read stream
const readStream = fs.createReadStream('input.txt', { encoding: 'utf8' });
readStream.on('data', (chunk) => console.log(chunk));
readStream.on('end', () => console.log('Done reading'));

// Write stream
const writeStream = fs.createWriteStream('output.txt');
writeStream.write('Hello\n');
writeStream.end('World');

// Transform stream
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  }
});

// Pipe streams together
await pipelineAsync(
  fs.createReadStream('input.txt'),
  upperCaseTransform,
  fs.createWriteStream('output.txt')
);

npm and Package Management

# Initialize project
npm init -y

# Install dependencies
npm install express
npm install --save-dev typescript

# Run scripts
npm run build
npm test

# Update packages
npm update
npm outdated

# Audit security
npm audit
npm audit fix

Environment Variables

// Access environment variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;

// Load from .env file
import 'dotenv/config';
// Now process.env has values from .env

// .env file (never commit!)
// DATABASE_URL=postgresql://localhost/mydb
// API_KEY=secret-key-here

Error Handling

// Try-catch for async/await
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error('Fetch failed:', error.message);
    throw error; // Re-throw or handle
  }
}

// Uncaught exceptions
process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
  process.exit(1);
});

// Unhandled promise rejections
process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
});

Best Practices

Do's

// ✅ Use node: prefix for built-in modules
import fs from 'node:fs';
import path from 'node:path';

// ✅ Use async/await over callbacks
const data = await fsPromises.readFile('file.txt', 'utf8');

// ✅ Handle errors properly
try { /* ... */ } catch (error) { /* ... */ }

// ✅ Use environment variables for config
const port = process.env.PORT || 3000;

// ✅ Use streams for large files
pipeline(readStream, transform, writeStream);

Don'ts

// ❌ Don't use sync methods in servers
fs.readFileSync('file.txt'); // Blocks the event loop!

// ❌ Don't hardcode secrets
const apiKey = 'sk-1234567890'; // Use env vars!

// ❌ Don't ignore errors
fs.readFile('file.txt', (err, data) => {
  console.log(data); // What if err?
});

// ❌ Don't use callback hell
fs.readFile('a.txt', (err, a) => {
  fs.readFile('b.txt', (err, b) => {
    fs.readFile('c.txt', (err, c) => {
      // Deep nesting!
    });
  });
});

Summary Table

ModulePurposeKey Methods
fsFile systemreadFile, writeFile, readdir
pathPath utilitiesjoin, resolve, extname
eventsEvent handlingon, emit, once
streamStreaming dataReadable, Writable, Transform
httpHTTP server/clientcreateServer, request
processProcess infoenv, argv, exit

Conclusion

Node.js brings JavaScript to the server with:

  • Non-blocking I/O — Handle thousands of concurrent connections
  • Event-driven architecture — Efficient and scalable
  • Rich standard library — File system, streams, networking, crypto
  • Massive ecosystem — npm with millions of packages
  • Full-stack JavaScript — Same language on frontend and backend

Master the core modules, embrace async/await, and follow best practices for production-ready Node.js applications.

#nodejs #javascript #backend #server #web-development