JSON in Node.js: Reading and Writing Files

Intermediate
12 min

JSON in Node.js: Reading and Writing Files

On the server, JSON is not only something you receive over HTTP; it is how configuration, seed data, caches and small datasets live on disk. In this lesson you will read JSON files with the fs module, import them as modules, write them back safely, and handle the two failures that occur constantly in practice: a missing file and a malformed one.

Reading a JSON File

Node.js has no readJson function. Reading JSON is always two steps: read the text, then parse it. The modern API is the promise-based node:fs/promises module:

javascript
import { readFile } from "node:fs/promises"; const text = await readFile("./data/users.json", "utf8"); const users = JSON.parse(text); console.log(users.length, users[0].name);

Always pass the "utf8" encoding. Without it readFile returns a Buffer; JSON.parse would still coerce it to a string, but being explicit avoids surprises with other encodings. For scripts and start-up code where blocking is acceptable, readFileSync from node:fs does the same job synchronously:

javascript
import { readFileSync } from "node:fs"; const config = JSON.parse(readFileSync(new URL("./config.json", import.meta.url), "utf8"));

The new URL(..., import.meta.url) form resolves the path relative to the current module rather than the working directory, which is what you want for files that ship with your code. In CommonJS use path.join(__dirname, "config.json") instead.

Importing JSON as a Module

For static files that never change at runtime, the module system can load JSON for you:

| Module system | Syntax | Notes | |---------------|--------|-------| | CommonJS | const pkg = require("./package.json"); | Parsed once and cached; every caller shares the same object | | ES modules (Node.js 22+) | import pkg from "./package.json" with { type: "json" }; | Import attribute is mandatory; only a default import is available |

Because the result is cached, mutating it (pkg.version = "2.0.0") changes what every other module sees for the rest of the process. Treat imported JSON as read-only and use fs for anything you intend to modify.

Writing JSON Files Safely

Writing is JSON.stringify followed by writeFile. Two habits make the output pleasant for humans and version control: indent with two spaces and end the file with a newline. A third habit protects data: write to a temporary file and rename it over the original, because rename is atomic on the same filesystem, so a crash mid-write leaves the old file intact rather than a truncated one.

javascript
import { writeFile, rename } from "node:fs/promises"; export async function saveJson(path, data) { const tmp = `${path}.tmp`; await writeFile(tmp, JSON.stringify(data, null, 2) + "\n", "utf8"); await rename(tmp, path); }

writeFile replaces the whole file. There is no way to append to a JSON array in place, which is one reason JSON Lines exists for append-only data (covered later in this course).

Handling Missing and Invalid Files

Two different errors hide behind the same try block, and they deserve different responses. A missing file (ENOENT) is often normal on first run and should fall back to a default; invalid JSON is corruption or a typo and should fail loudly with the file name:

javascript
import { readFile } from "node:fs/promises"; export async function loadJson(path, fallback = {}) { try { return JSON.parse(await readFile(path, "utf8")); } catch (err) { if (err.code === "ENOENT") return fallback; if (err instanceof SyntaxError) { throw new Error(`Invalid JSON in ${path}: ${err.message}`); } throw err; } }

Recent Node.js versions include the location in the message, for example Expected double-quoted property name in JSON at position 42 (line 3 column 5), which points you to the exact character to fix. A file saved by some Windows editors starts with an invisible byte order mark; if the error reports an unexpected token at the very start of the file, strip it with text.replace(/^/, "") before parsing.

Common Mistakes

  • Using a JSON file as a database. Two requests that read-modify-write the same file concurrently will lose one update. JSON files suit configuration and small single-writer data; anything with concurrent writers belongs in SQLite or a real database.
  • Parsing gigantic files in one go. readFile loads everything into memory and V8 caps a single string at roughly 512 MB. Stream large inputs instead.
  • Relying on require for data that changes. The cache means edits on disk are never seen until the process restarts.
  • Omitting "utf8" in writeFile. Strings default to UTF-8, but stating it keeps reads and writes symmetric.
Quick Quiz
Question 1 of 3

What does `readFile("data.json")` return when no encoding is given?

Key Takeaways

  • Reading JSON in Node.js is always readFile + JSON.parse; pass "utf8" to get a string.
  • require("./x.json") and import x from "./x.json" with { type: "json" } load static files once and cache them.
  • Write with JSON.stringify(data, null, 2) + "\n" and use a temp file plus rename for crash-safe saves.
  • Handle ENOENT (missing file) and SyntaxError (invalid JSON) as separate cases.
  • JSON files are fine for config and small single-writer data, not for concurrent writes or huge datasets.

Next lesson: Fetching and Sending JSON with fetch and axios — request JSON from APIs in the browser and Node.js, post JSON bodies, and handle errors properly.

JSON in Node.js: Reading and Writing Files - JSON | CodeYourCraft | CodeYourCraft