How File Watching Works in JSON Server for Development
JSON Server uses the chokidar library to monitor your database file in development mode, automatically reloading the mock API whenever external edits occur while using an Observer pattern with lifecycle hooks to prevent infinite reload loops when the server writes to its own database.
JSON Server provides instant hot-reloading of your mock REST API by monitoring the underlying JSON database file for changes. This file watching capability, implemented primarily in src/bin.ts and src/adapters/observer.ts, ensures that any manual edits to your db.json are immediately reflected without restarting the server. According to the typicode/json-server source code, the feature is automatically enabled in non-production environments through a sophisticated adapter chain that distinguishes between external file modifications and internal write operations.
The Three-Layer File Watching Architecture
The implementation splits responsibilities across three coordinated modules that bridge the filesystem, the database layer, and the HTTP server.
Observer Adapter (src/adapters/observer.ts) wraps any LowDB adapter and exposes four critical lifecycle hooks: onReadStart, onReadEnd, onWriteStart, and onWriteEnd. These callbacks allow the CLI to track exactly when the application itself is reading from or writing to the database file.
Normalized Adapter (src/adapters/normalized-adapter.ts) handles data normalization—such as ID coercion and $schema stripping—before the data reaches LowDB. This concrete adapter is what gets wrapped by the Observer.
Chokidar Watcher (src/bin.ts) uses the robust chokidar library (imported on line 7) to subscribe to native filesystem change events. The watcher is only registered when NODE_ENV !== "production" to avoid unnecessary I/O overhead in deployed environments.
CLI Implementation in src/bin.ts
The file watching logic is orchestrated from the CLI entry point, which coordinates the server startup with the filesystem monitor.
The system first creates an adapter chain:
const observer = new Observer(new NormalizedAdapter(adapter));
const db = new Low<Data>(observer, {});
await db.read();
Immediately after the server starts, the CLI registers four callbacks on the observer (lines 91–108). The onWriteStart and onWriteEnd hooks toggle a writing boolean flag, while onReadStart and onReadEnd track endpoint changes for console logging. This state management is crucial for distinguishing user edits from server-generated writes.
Inside the development-only block (lines 85–124), the chokidar watcher is initialized:
watch(file).on("change", () => {
if (!writing) {
db.read().catch((e) => {
if (e instanceof SyntaxError) {
hadReadError = true;
console.log(chalk.red(["", `Error parsing ${file}`, e.message].join("\n")));
} else {
console.log(e);
}
});
}
});
When the filesystem detects a change, the callback checks the writing flag. If the server itself is not currently writing to the file, db.read() reloads the JSON content into memory, instantly updating the available API routes.
Preventing Infinite Reload Loops
Without safeguards, JSON Server would enter an infinite loop: writing to db.json (via POST/PUT requests) triggers a file change event, which triggers a reload, which writes again. The Observer pattern breaks this cycle.
The writing flag acts as a mutex. When the server handles a mutating request:
onWriteStartsetswriting = true- LowDB writes to the filesystem
- Chokidar detects the change but ignores it because
writingis true onWriteEndsetswriting = false
This ensures that only external edits—those made by your code editor or other processes—trigger a reload. The hadReadError boolean complements this by tracking whether the previous read failed due to a syntax error, ensuring the CLI re-logs routes once valid JSON is restored.
Handling Syntax Errors and Route Changes
The file watcher includes graceful error handling for malformed JSON. When db.read() throws a SyntaxError, the catch block logs a red error message to the console and sets hadReadError = true. The system does not crash; instead, it waits for the next file change event.
The onReadEnd callback compares the previous endpoint keys (prevEndpoints) with the new set (nextEndpoints). If the structure changed or if a previous read error occurred, the CLI calls logRoutes(data) to print the updated route table, giving developers immediate visual feedback on the current API surface.
Development Usage Examples
Start JSON Server in development mode to enable automatic watching:
json-server db.json
# Output includes "Watching db.json...", and any external edit to db.json
# instantly updates the API routes.
Note that the deprecated --watch / -w flag is still accepted for backward compatibility but serves only to print a notice; file watching is now the default behavior in development (lines 83–90 of src/bin.ts).
To recreate the watching logic in a custom script:
import { watch } from "chokidar";
import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
// 1️⃣ Set up LowDB
const adapter = new JSONFile("db.json");
const db = new Low(adapter);
await db.read();
// 2️⃣ State flags (mirroring src/bin.ts)
let writing = false;
let hadReadError = false;
// 3️⃣ Watch the file (dev only)
watch("db.json").on("change", async () => {
if (writing) return; // ignore self-writes
try {
await db.read(); // reload
console.log("Reloaded:", db.data); // custom handling
} catch (e) {
if (e instanceof SyntaxError) {
hadReadError = true;
console.error("❌ Invalid JSON:", e.message);
} else {
console.error(e);
}
}
});
For full parity with JSON Server's internal implementation, use the built-in Observer to hook into write operations:
import { Observer } from "./src/adapters/observer.ts";
import { NormalizedAdapter } from "./src/adapters/normalized-adapter.ts";
const rawAdapter = new JSONFile("db.json");
const observer = new Observer(new NormalizedAdapter(rawAdapter));
observer.onWriteStart = () => (writing = true);
observer.onWriteEnd = () => (writing = false);
Summary
- Development-only activation: File watching is automatically disabled in production when
NODE_ENV === "production"to eliminate unnecessary filesystem overhead. - Observer pattern: The
Observerclass insrc/adapters/observer.tswraps the database adapter with hooks that track read/write lifecycle events. - Reload loop prevention: A
writingflag set viaonWriteStartandonWriteEndcallbacks ensures that server-generated writes do not trigger redundant reloads. - Filesystem monitoring: The
chokidarlibrary watches the database file and triggersdb.read()only for external modifications. - Graceful degradation: Syntax errors are caught and logged without crashing the server, and the route table is automatically re-printed whenever the API structure changes.
Frequently Asked Questions
Does JSON Server watch files in production?
No. According to the logic in src/bin.ts (lines 85–124), the chokidar watcher is only registered when NODE_ENV !== "production". This design prevents performance issues and accidental file monitoring in production deployments where the database file should remain static or be managed by separate processes.
Why doesn't my API reload infinitely when I use POST or PUT requests?
The Observer pattern prevents this. When JSON Server writes to db.json in response to a mutating request, the onWriteStart hook sets a writing flag to true. The chokidar change event handler checks this flag and skips the reload if writing is true. Once the write completes, onWriteEnd resets the flag, re-enabling the watcher for external edits.
How do I disable file watching in development?
JSON Server v1+ enables watching by default in development and does not provide a CLI flag to disable it. The deprecated --watch / -w flags are accepted but only print a deprecation notice; they do not toggle the behavior. To disable watching, you would need to set NODE_ENV=production, though this changes other behaviors as well.
What happens if I save invalid JSON to the database file while the server is running?
The watcher catches SyntaxError exceptions during the db.read() call, logs a red error message to the console, and stores the error state in hadReadError. The server continues running with the previous valid data. On the next successful file change (once you fix the JSON), the server reloads the data and automatically re-logs the available routes to confirm recovery.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →