Phase 1 · Node.js FoundationsModule 6~48 min read

Files, Paths & Essential Core APIs

Read, write, and organize local data safely using Node.js core modules and promise-based file-system operations.

What you'll learn

Files are a durable boundary between your program and the operating system. You will use promise-based APIs, build portable paths, serialize JSON, and handle real file failures without blocking the event loop.

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

  • Read and write UTF-8 files with node:fs/promises
  • Build cross-platform paths with node:path
  • Distinguish module-relative locations from the current working directory
  • Persist and recover structured JSON data safely

Core mental model

Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.

ConceptWhat it meansDecision rule
Async file APIA promise settles after the file operation completesPrefer fs/promises in application paths that must remain responsive
Working directoryprocess.cwd() is where the process was launchedUse it for user-selected project-relative input
Module directoryThe location of the source module itselfUse it for assets shipped beside the module
SerializationConverts in-memory data to a stable text representationValidate parsed JSON before trusting its shape

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. Decide whether a path belongs to the caller's working directory or to the module.
  2. Construct the path with path utilities or a file URL instead of manual separators.
  3. Perform the operation directly and handle its error; avoid a separate exists check race.
  4. Specify text encoding explicitly and validate parsed data.
  5. Write a complete replacement to a temporary sibling before renaming when durability matters.
  6. Test missing, malformed, empty, and permission-denied paths.

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

Guided code lab

Read a module-relative text file

new URL resolves the asset beside this module no matter which folder launched the process.

read-template.js
import { readFile } from "node:fs/promises";

const templateUrl = new URL("./welcome.txt", import.meta.url);

try {
  const message = await readFile(templateUrl, "utf8");
  console.log(message.trim());
} catch (error) {
  if (error.code === "ENOENT") {
    console.error("welcome.txt is missing");
    process.exitCode = 1;
  } else {
    throw error;
  }
}

Create a portable report path

path.join uses the correct separator for the current platform. recursive mkdir makes repeated setup safe.

write-report.js
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";

const outputDirectory = join(process.cwd(), "reports");
const outputFile = join(outputDirectory, "summary.json");
const report = { generatedAt: new Date().toISOString(), lessons: 6 };

await mkdir(outputDirectory, { recursive: true });
await writeFile(outputFile, JSON.stringify(report, null, 2) + "
", "utf8");
console.log("Saved", outputFile);

Load JSON with shape validation

JSON syntax can be valid while the data shape is wrong. Parse errors and validation errors need different messages.

load-tasks.js
import { readFile } from "node:fs/promises";

export async function loadTasks(fileUrl) {
  try {
    const text = await readFile(fileUrl, "utf8");
    const value = JSON.parse(text);
    if (!Array.isArray(value)) throw new TypeError("tasks must be an array");
    return value;
  } catch (error) {
    if (error.code === "ENOENT") return [];
    if (error instanceof SyntaxError) {
      throw new Error("Tasks file contains invalid JSON", { cause: error });
    }
    throw error;
  }
}

Production practice

Use promises

Prefer fs/promises for clear async control flow; reserve synchronous APIs for controlled startup or one-off scripts where blocking is acceptable.

Validate data

Treat every file as external input. Validate parsed types, required fields, ranges, and identifiers.

Name the path base

Make working-directory-relative versus module-relative intent obvious in variable names and documentation.

Common failure mode

Checking whether a file exists before opening it introduces a race: the file can change between the check and the operation. Attempt the operation and handle the resulting error.

Independent workshop

Build a JSON-backed notes repository with list, add, and remove operations that works on Windows, macOS, and Linux.

Your finished workshop must include:

  • Promise-based file operations
  • Portable path construction
  • Empty-state behavior for a missing file
  • Clear errors for malformed JSON and invalid records
  • Pretty-printed output ending with a newline

Definition of done

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

Recap & quick check

Key takeaways

  • fs/promises keeps file workflows composable with async/await
  • Path intent matters as much as path syntax
  • path utilities avoid platform-specific separators
  • JSON parsing requires shape validation
  • Handle operation errors instead of pre-checking

Quick check

1. Relative fs paths are normally resolved against what?

2. Which API family returns promises for file operations?

3. Does valid JSON guarantee the expected application shape?

4. Why use path.join?

Next: Async Foundations, Events & Errors