Skip to main content

🧰 Core Node.js Modules (fs, path, http)

Before you reach for a single npm package, Node hands you a powerful built-in toolbox. In this lesson you will read and write files, stream large ones without exhausting memory, build paths that work on any operating system, and stand up a working web server — all with modules that ship inside Node itself.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Read and write files with the fs module across its sync, callback, and promise APIs
  • Explain when and why to use streams instead of loading whole files into memory
  • Build reliable, cross-platform paths with the path module
  • Create an http server that routes requests and returns HTML and JSON
  • Combine fs, path, and http to build a small static file server

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Build a working HTTP server that serves both web pages and a JSON API.

In This Lesson

What Are Core Modules?

Core modules are built into Node.js — no npm install required. They give you standardized access to the file system, networking, paths, operating-system info, and more. You import them with require (or import), and modern style prefixes them with node: to make it obvious they are built-ins rather than npm packages:

const fs = require('node:fs');
const path = require('node:path');
const http = require('node:http');
const os = require('node:os');
flowchart TD A[Node.js core modules] --> B[fs
file system] A --> C[path
path handling] A --> D[http / https
web servers] A --> E[os
system info] A --> F[events
EventEmitter] A --> G[stream
chunked data]
🚗 A useful analogy: Core modules are the toolkit that comes bundled with your car — the jack, the wrench, the spare — designed to fit the system exactly and ready the moment you need them, no trip to the store required.

This lesson focuses on the three you will reach for constantly: fs, path, and http.

The File System Module (fs)

The fs module lets you read, write, update, and delete files and directories. Most of its methods come in three flavors, and choosing the right one matters:

FlavorStyleUse when
Synchronousfs.readFileSync()Startup scripts / CLI tools where blocking is fine
Callbackfs.readFile(…, cb)Legacy async code
Promisefs.promises.readFile()Preferred — clean async/await in servers

Reading files — the three flavors

const fs = require('node:fs');
const fsp = require('node:fs/promises'); // the promise API

// 1. Synchronous — blocks the event loop until done
try {
  const data = fs.readFileSync('file.txt', 'utf8');
  console.log(data);
} catch (err) {
  console.error('Read failed:', err);
}

// 2. Callback — the classic async style
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) return console.error('Read failed:', err);
  console.log(data);
});

// 3. Promise + async/await — the modern default
async function show() {
  try {
    const data = await fsp.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('Read failed:', err);
  }
}
show();

⚠️ Avoid the Sync methods on a server

A synchronous call like readFileSync freezes Node's single thread until the disk responds — meaning every other request waits too. Reserve the Sync variants for one-off scripts and startup config loading; use the promise API everywhere in a running server.

Writing, appending, and directories

const fsp = require('node:fs/promises');

async function fileWork() {
  // Overwrite (or create) a file
  await fsp.writeFile('output.txt', 'Hello, Node.js!');

  // Append without erasing existing content
  await fsp.appendFile('logs.txt', `\nEntry at ${new Date().toISOString()}`);

  // Create a directory only if it isn't there yet
  await fsp.mkdir('data', { recursive: true });

  // List a directory's contents
  const files = await fsp.readdir('data');
  console.log('data/ contains:', files);
}
fileWork().catch(console.error);

The fs module also offers rename, unlink (delete), stat (file info), watch (react to changes), and stream creators covered next. Think of fs as your application's filing clerk — storing, fetching, and organizing documents on demand.

Streams for Large Files

readFile loads an entire file into memory before you can touch it — fine for a 2 KB config, disastrous for a 2 GB log. Streams solve this by delivering the file in small chunks, so you process data as it arrives and never hold the whole thing at once.

flowchart LR A[Large file on disk] -->|chunk 1| B[Read stream] A -->|chunk 2| B A -->|chunk 3| B B -->|process each chunk| C[Your code / response]

Reading and writing with streams

const fs = require('node:fs');

const readStream = fs.createReadStream('large-file.txt', { encoding: 'utf8' });

readStream.on('data', (chunk) => {
  console.log(`Got ${chunk.length} characters`);
});
readStream.on('end', () => console.log('Done reading'));
readStream.on('error', (err) => console.error('Stream error:', err));

Piping — the killer feature

pipe() connects a read stream directly to a write stream, copying data chunk-by-chunk with almost no memory overhead:

const fs = require('node:fs');

// Copy a huge file efficiently — no full load into memory
fs.createReadStream('input.txt')
  .pipe(fs.createWriteStream('output.txt'))
  .on('finish', () => console.log('File copied'));
🚰 Plumbing analogy: Loading a whole file is like filling a giant tank before you use any water. Streaming is a pipe — water (data) flows through continuously in small amounts, so you never need a tank big enough to hold it all.

The path Module

Windows uses backslashes (\), macOS and Linux use forward slashes (/). Hand-concatenating paths with string + is a bug waiting to happen. The path module builds and dissects paths correctly on every OS:

const path = require('node:path');

// Join segments using the right separator for this OS
const full = path.join(__dirname, 'data', 'users.json');
// Windows: C:\app\data\users.json   POSIX: /app/data/users.json

// Resolve to an absolute path, collapsing .. and .
const abs = path.resolve('data', '..', 'config', 'settings.json');

// Pull a path apart
path.dirname('/users/files/doc.txt');   // '/users/files'
path.basename('/users/files/doc.txt');  // 'doc.txt'
path.extname('/users/files/doc.txt');   // '.txt'

// Structured parse
path.parse('/users/files/doc.txt');
// { root: '/', dir: '/users/files', base: 'doc.txt', ext: '.txt', name: 'doc' }

Two variables you will use often: __dirname (absolute path of the current file's folder) and __filename (absolute path of the current file). Building paths relative to __dirname makes your code work no matter where it is launched from.

💡 Always use path.join, never string concatenation

Writing __dirname + '/data/' + name breaks on Windows and mishandles edge cases like doubled slashes. path.join(__dirname, 'data', name) is correct everywhere — treat it as the default.

The http Module

The http module can create a web server with no framework at all. Every request invokes your handler with two objects: req (what the client sent) and res (what you send back).

A minimal server with routing

const http = require('node:http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html');

  if (req.url === '/') {
    res.statusCode = 200;
    res.end('<h1>Home Page</h1>');
  } else if (req.url === '/about') {
    res.statusCode = 200;
    res.end('<h1>About Page</h1>');
  } else {
    res.statusCode = 404;
    res.end('<h1>404 Not Found</h1>');
  }
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}/`);
});

Handling a POST with JSON

Request bodies arrive in chunks, so you collect them, then parse on end:

const http = require('node:http');

const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/api/users') {
    let body = '';
    req.on('data', (chunk) => { body += chunk; });
    req.on('end', () => {
      try {
        const user = JSON.parse(body);
        res.setHeader('Content-Type', 'application/json');
        res.statusCode = 201; // Created
        res.end(JSON.stringify({ message: 'User created', user }));
      } catch {
        res.statusCode = 400; // Bad Request
        res.end(JSON.stringify({ error: 'Invalid JSON' }));
      }
    });
  } else {
    res.statusCode = 404;
    res.end('Not Found');
  }
});

server.listen(3000);
sequenceDiagram participant C as Client participant S as http server C->>S: POST /api/users { name } Note over S: req.on('data') collects chunks Note over S: req.on('end') parses JSON S-->>C: 201 Created { message, user }

📖 Note on real projects

You can build servers with raw http, but the manual routing and body-parsing get tedious fast. Frameworks like Express wrap the http module to handle these chores for you — which is exactly why the next module introduces it. Learning raw http first means you will understand what Express is doing under the hood.

For secure servers, the near-identical https module adds TLS encryption — you pass it a certificate and private key, and everything else looks the same.

Worked Example: A File Server

Now combine all three modules into a small static file server. path builds the file location, fs streams it, and http delivers it — with correct content types and error handling:

const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');

const PUBLIC_DIR = path.join(__dirname, 'public');

const MIME = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'text/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
};

const server = http.createServer((req, res) => {
  // Map the URL to a file, defaulting '/' to index.html
  const requested = req.url === '/' ? '/index.html' : req.url;
  const filePath = path.join(PUBLIC_DIR, requested);
  const contentType = MIME[path.extname(filePath)] || 'text/plain';

  const stream = fs.createReadStream(filePath);

  stream.on('open', () => {
    res.statusCode = 200;
    res.setHeader('Content-Type', contentType);
    stream.pipe(res); // stream the file straight to the response
  });

  stream.on('error', () => {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/html');
    res.end('<h1>404 Not Found</h1>');
  });
});

server.listen(process.env.PORT || 3000, () => {
  console.log('File server on http://localhost:3000/');
});

Streaming the file with pipe keeps memory flat even for large assets, and starts sending bytes to the browser immediately instead of waiting for the whole file to load.

⚠️ Security: prevent path traversal

A malicious request like /../../etc/passwd could try to escape public/. In production, always verify the resolved path stays inside your intended directory — for example, check that path.resolve(filePath).startsWith(PUBLIC_DIR) before serving. Never trust req.url blindly.

Hands-on Exercise

🏋️ Build a Mini API Server

Objective: Combine http and JSON handling to serve both a web page and an API endpoint.

Instructions:

  1. Create server.js that listens on port 3000.
  2. Route / to an HTML response and /about to a second HTML page.
  3. Route /api/time to return JSON with the current ISO timestamp and the correct Content-Type.
  4. Return a proper 404 for anything else.
  5. Run it with node server.js and test each route in your browser.
💡 Hint

Branch on req.url. For the API route, set res.setHeader('Content-Type', 'application/json') and send JSON.stringify({ time: new Date().toISOString() }). Set res.statusCode before res.end().

✅ Solution
const http = require('node:http');

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html');
    res.end('<h1>Home</h1>');
  } else if (req.url === '/about') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html');
    res.end('<h1>About</h1>');
  } else if (req.url === '/api/time') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ time: new Date().toISOString() }));
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/html');
    res.end('<h1>404 Not Found</h1>');
  }
});

server.listen(3000, () => console.log('http://localhost:3000/'));

Visiting /api/time returns something like {"time":"2026-08-01T12:00:00.000Z"}, while / and /about render HTML and an unknown path returns 404.

🎯 Quick Quiz

Question 1: Why should a running server avoid fs.readFileSync?

Question 2: What is the main advantage of streaming a large file with createReadStream over readFile?

Question 3: Which path call correctly builds a cross-platform path to data/users.json inside the current file's folder?

Summary & Quiz

🎉 Key Takeaways

  • Core modules ship with Node — no install needed; prefix them with node:.
  • fs reads and writes files in sync, callback, and promise flavors — prefer the promise API in servers.
  • Streams process large files in chunks; pipe() connects them with minimal memory.
  • path builds cross-platform paths — always use path.join, never string concatenation.
  • http creates servers by handling req/res; bodies arrive in chunks you assemble.
  • Combining fs + path + http yields real functionality like a static file server — but guard against path traversal.

📚 Further Reading

🚀 What's Next?

You have wrapped up Node's backend fundamentals. Next, the course shifts languages to compare a second backend ecosystem — starting with Python syntax and data types — so you can see how the same concepts translate across stacks.

🎉 Excellent work!

You can now read files, stream data, handle paths safely, and serve HTTP — all with zero dependencies. That is the real foundation every Node framework is built on.