2.1 · Native modules: fs, path, os

Level 2 · Node.js Core

2.1Native modules: fs, path, os

Goal: Use Node's native modules (asynchronous fs, portable path, os): system capabilities with no external dependency.
Estimated time: 11 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • Built in, nothing to install: fs, path, os ship with Node and are imported by name — fewer dependencies, lighter, safer.
  • fs = asynchronous by default: fs.promises + await never blocks the event loop — Sync blocks the server for everyone.
  • path makes portable, os informs: path.join uses the right separator on each system, os exposes platform and memory — never concatenate paths by hand.

Ongoing mini-case — Salim inventories reports in Algiers

Salim, a junior developer in Algiers, has to equip his team: every week, dozens of text reports land in a shared folder, and they must be listed (name, size, date) in an inventory file. He writes his first Node script with nothing installed: fs reads and writes files, path builds paths that work on his Windows machine as well as on the team's Linux server, os prints the platform for the log. This same project returns in 2.2 (signalling progress with events), 2.3 (large reports processed as streams) and 2.4 (serving the inventory over HTTP).

Level 2 thread: this same Salim project returns in the lesson, the diagram, the script and the 48h challenge — and continues in 2.2, 2.3 and 2.4.

1. Native modules, nothing to install

Node provides native (built-in) modules: integrated libraries, available with nothing to install, that give access to system capabilities — files, network, operating system. That is what makes Node much more than a simple JavaScript runner. You import them by name alone (require('fs'), with the explicit node: prefix possible: require('node:fs')). The most used is fs (file system): reading, writing, modifying, deleting files and folders. The benefit is twofold: covering common needs with no external dependency, and understanding the foundation of libraries (Express rests on the http module). Salim verified it: his inventory needs no npm package.

2. fs: async first, Sync almost never

Key point: most fs functions come in two versions — asynchronous (non-blocking, prefer it: fs.readFile with callback, or better fs.promises.readFile with await) and synchronous (blocking, suffixed Sync: fs.readFileSync). On a server, use the asynchronous version so you don't block the event loop: a single slow readFileSync freezes the server for every client. Sync is only justified in specific cases — small one-off scripts, loading config at startup. So Salim writes his inventory with fs.promises + async/await: modern, readable, non-blocking.

3. path for portability, os for the system

The path module handles paths in a portable way: the separator is / on macOS/Linux but \ on Windows, so you never assemble a path by hand. path.join(__dirname, 'data', 'fichier.txt') uses the right separator for the system; path.resolve() gives an absolute path, path.basename(), path.extname() and path.dirname() extract its parts. The os module exposes the system: os.platform(), os.cpus() (useful for clustering, section 5-2), os.totalmem() / os.freemem(), os.homedir(), os.tmpdir(). Other natives exist (crypto, util, url…): no need to memorise them all, but know they exist and check the official docs.

Common misinterpretation: “To go fast, I'll use the Sync versions everywhere — it's easier to read”. No. On a server, every synchronous call blocks Node's single thread for the whole operation: while readFileSync reads a file, the server answers no other user. The apparent “simpler” option is a serious performance fault as soon as there are concurrent clients. The rule: on a server, always async (fs.promises + await); Sync is reserved for small one-off scripts and loading config at startup.
The three everyday natives System with no dependency fsRead/write asynchronously (promises + await)+pathPortable paths (join, resolve, basename)+osSystem info (platform, CPU, memory) Native = imported by name, zero installation.
System with no dependency
Text version of the diagram (SEO / screen readers / translation): 1) Salim imports the NATIVES by name (require('fs'), require('path'), require('os')) — nothing to install. 2) He reads and writes the inventory with fs.promises + await: ASYNCHRONOUS, the server stays responsive for everyone. 3) He builds paths with path.join: PORTABLE between his Windows machine and the team's Linux. 4) He logs platform and memory with os. Lesson: natives cover common needs with no dependency — and underpin libraries like Express.

🔎 Concrete example — 🔎 Concrete example — Salim's async inventory

Three natives combined, as in Salim's script:

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

const dossier = path.join(__dirname, 'rapports');
const fichiers = await fs.readdir(dossier);
const inventaire = path.join(dossier, 'inventaire.txt');
await fs.writeFile(inventaire, fichiers.join('\n'));
console.log('Plateforme :', os.platform());

Reading: no package installed, portable paths, non-blocking write.

Lesson: fs.promises + await on a server, Sync reserved for scripts.

Section vocabulary

Native (built-in) modules
Libraries built into Node, imported by name with no installation: fs, path, os, http, crypto, events, stream…

Simple example: Salim imports fs, path and os without touching npm.

What it is not: a third-party npm package to install and update.

Reflex: common need = check the native module before installing.

fs (file system)
Module for reading/writing files and folders, in async version (preferred, fs.promises + await) or sync version (Sync, blocking).

Simple example: Salim lists the reports with fs.readdir and fs.writeFile.

What it is not: a web routing module (that's Express, level 3).

Reflex: files on a server = fs.promises + await.

Async vs sync (fs)
Async (non-blocking) lets Node serve other requests during the operation; sync (Sync) blocks the single thread.

Simple example: Salim's inventory is written without freezing the server.

What it is not: two interchangeable syntaxes with no consequence.

Reflex: server = async; Sync = scripts and startup config.

path
Module for portable path handling (join, resolve, basename, extname, dirname) that manages each system's separator.

Simple example: path.join(__dirname, 'rapports') works on Salim's Windows and the team's Linux.

What it is not: manual concatenation with hard-coded slashes.

Reflex: a path to build = path.join.

os
Module for system information: platform, processors (os.cpus), memory, standard folders (homedir, tmpdir).

Simple example: Salim logs os.platform() and os.freemem() with his inventory.

What it is not: a file-reading module.

Reflex: machine info (clustering, diagnostics) = think os.

In practice — In practice — Using native modules

  1. Read and write a file with fs.promises and async/await (async, non-blocking); compare with fs.readFileSync (sync, blocking).
  2. Build portable paths with path.join(__dirname, 'dossier', 'fichier.txt') rather than concatenating by hand.
  3. Extract information from a path: path.basename, path.extname, path.dirname.
  4. Print system info with os: os.platform(), os.cpus().length, os.freemem().
You use Node's native modules (fs for async files, path for portable paths, os for the system) — system capabilities with no external dependency.

Ready-to-say script — justifying all-native to your lead (30 s)

“For the reports inventory, I installed nothing: fs reads and writes files asynchronously with fs.promises, path builds paths that work on Windows as on Linux, and os logs the platform. Zero dependencies, so zero updates to track — and these are the same building blocks Express is built on.”

Say it while showing the script running on both systems; the convincing argument is the absence of dependencies.

Copy-paste template — async folder inventory

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

async function inventaire(nomDossier) {
  const dossier = path.join(__dirname, nomDossier);
  const fichiers = await fs.readdir(dossier);
  const lignes = [];
  for (const f of fichiers) {
    const chemin = path.join(dossier, f);
    const stats = await fs.stat(chemin);
    lignes.push(f + ' | ' + stats.size + ' bytes | ' + path.extname(f));
  }
  const sortie = path.join(dossier, 'inventaire.txt');
  await fs.writeFile(sortie, lignes.join('\n'));
  console.log('Inventory written on', os.platform(), ':', sortie);
}

inventaire('rapports').catch(console.error);

Quick personal script

A small one-off utility (renaming, sorting files): Sync is acceptable there, the script finishes right away.

Mixed Windows/Linux team

Salim bans hard-coded slashes: every path goes through path.join to run everywhere with no changes.

Production server

fs.promises + await everywhere: no Sync call on the request path, the event loop stays free.

Machine diagnostics

Before deploying, os.cpus().length and os.freemem() size the deployment (see clustering, section 5-2).

✓ Best practices

  • Import natives by name (node: prefix possible) with nothing to install.
  • Use fs.promises + await on a server; reserve Sync for scripts and config.
  • Build every path with path.join, never by concatenation.

✗ Common pitfalls

  • Using the Sync versions on a server (blocking for every client).
  • Concatenating paths by hand (Windows/Linux portability bug).
  • Installing an npm package for what a native already does.

Key takeaways

  • NATIVE MODULES (fs, path, os, http, crypto…) are built into Node (no installation) and give access to the system; imported by name.
  • fs (files): prefer ASYNC (fs.promises + await, non-blocking) on a server; sync (Sync) blocks the event loop (reserved for scripts/config).
  • path: handle paths PORTABLY (path.join picks the right separator / vs \) — never concatenate paths by hand.
  • os: system info (platform, os.cpus for clustering, memory). Natives are the FOUNDATION of libraries (Express rests on http).

48h challenge — apply it tomorrow

Write the inventory script from the template above on a real folder (your reports, your exports): list name, size and extension asynchronously, with path.join paths. Ban every Sync call from the script.

Section exit checklist
  • I can name the 3 natives and their role in one sentence each.
  • I can write an fs.promises + await script with no Sync.
  • I produced a real inventory with portable paths.
Self-rating 1-5: do I use async on a server?do I ban hand-concatenated paths?

Frequently asked questions

fs async or sync version: which one to use, and when is sync acceptable?

On a server, always async (fs.promises + await): Node is single-threaded, and a synchronous call (readFileSync…) blocks that thread for the whole operation — the server then answers no other client. That is exactly what Node's non-blocking model avoids. Sync is only acceptable in two cases: small one-off scripts (no server, no concurrent users) and loading config at startup (once, before listening). Salim's rule: Sync on a request path = performance fault.

Why use the path module rather than building paths by hand?

Because manual concatenation breaks as soon as you change systems: the separator is / on macOS/Linux but \ on Windows, and you quickly produce double separators or glued parts (“file not found”). path.join(__dirname, 'data', nom) assembles with the right separator for the system and handles edge cases; path.resolve() produces an absolute, basename/extname/dirname split cleanly. Salim's code runs with no changes on his Windows machine and the team's Linux.

Check your understanding

Salim reads reports on a server used by the whole team. Which fs version?

Additional resources (optional, to go further)

Level 2 examFresh, harder questions to test what you've learned