How JSON Server Uses lowdb for In-Memory JSON Database Operations
JSON Server uses lowdb as an in-memory database with pluggable adapters to read from and write to JSON files, wrapping it with normalization and observer layers to handle schema validation and lifecycle events.
The typicode/json-server project leverages lowdb to provide a zero-configuration REST API over a simple JSON file. By combining lowdb’s lightweight adapter system with custom wrappers, JSON Server achieves file persistence, data normalization, and real-time change detection without requiring a separate database process.
Adapter Chain Architecture
JSON Server constructs a sophisticated adapter chain in src/bin.ts to prepare lowdb for production use. The CLI entry point builds three distinct layers around the raw file adapter.
File Adapter and Wrapper Layers
First, the system selects a file adapter based on the file extension. For standard JSON, it uses JSONFile; for JSON5, it uses DataFile. This adapter handles the actual disk I/O.
Next, the code wraps the file adapter in a NormalizedAdapter (src/adapters/normalized-adapter.ts), which enforces JSON Server’s data requirements. Finally, an Observer (src/adapters/observer.ts) wraps the normalized adapter to emit lifecycle events.
import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
import { NormalizedAdapter } from "./adapters/normalized-adapter.ts";
import { Observer } from "./adapters/observer.ts";
const file = "db.json";
const adapter = new JSONFile(file);
const observer = new Observer(new NormalizedAdapter(adapter));
const db = new Low<Data>(observer, {});
await db.read();
In-Memory Data Access and the Service Layer
Once initialized, the Low instance holds the entire database in db.data. JSON Server passes this instance to the HTTP layer (src/app.ts) and the core Service class (src/service.ts), which encapsulates all CRUD logic.
Read Operations
The Service class stores the lowdb instance in a private field #db. Read-only methods such as find, findById, and has access this.#db.data directly without triggering file system calls. Because lowdb maintains the JSON in memory, these lookups operate at memory speed.
export class Service {
#db: Low<Data>;
constructor(db: Low<Data>) { this.#db = db; }
#get(name: string) { return this.#db.data[name]; }
find(name: string, opts) {
const items = this.#get(name);
if (!Array.isArray(items)) return items;
// filtering logic...
return items;
}
}
Write Operations
Mutations follow a consistent pattern: modify the in-memory array, then persist. Methods like create, update, and delete push changes to this.#db.data and await this.#db.write() to flush to disk.
async create(name: string, data: Omit<Item, 'id'>) {
const items = this.#get(name);
if (!Array.isArray(items)) return;
const item = { id: randomId(), ...data };
items.push(item);
await this.#db.write();
return item;
}
Data Normalization and Schema Handling
Before data reaches lowdb, the NormalizedAdapter enforces JSON Server’s conventions. This adapter strips the $schema property, coerces numeric IDs to strings, and auto-generates missing IDs using randomId().
async read(): Promise<Data | null> {
const data = await this.#adapter.read();
if (data === null) return null;
delete data['$schema'];
for (const value of Object.values(data)) {
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item['id'] === 'number') item['id'] = item['id'].toString();
if (item['id'] === undefined) item['id'] = randomId();
}
}
}
return data as Data;
}
Observer Pattern for Lifecycle Events
The Observer adapter wraps any lowdb adapter to emit read/write lifecycle events. JSON Server’s CLI attaches handlers to these hooks to log endpoint changes and prevent race conditions during file writes.
observer.onWriteStart = () => { writing = true; };
observer.onWriteEnd = () => { writing = false; };
observer.onReadStart = () => { prevEndpoints = JSON.stringify(Object.keys(db.data).sort()); };
observer.onReadEnd = data => { /* log new endpoints */ };
Summary
- JSON Server uses lowdb as its in-memory data store, leveraging the
Lowclass to hold the entire JSON database in memory. - The adapter chain in
src/bin.tscombines file adapters (JSONFileorDataFile) withNormalizedAdapterfor schema handling andObserverfor lifecycle events. - The Service class (
src/service.ts) performs O(1) reads fromdb.dataand persists mutations viaawait db.write(). - Normalization ensures every item has a string
idand removes$schemabefore lowdb processes the data. - Observer hooks enable the CLI to detect endpoint changes and manage write-state concurrency.
Frequently Asked Questions
What is lowdb and why does JSON Server use it?
lowdb is a lightweight, local JSON database powered by Lodash. It provides an in-memory object interface with pluggable adapters for persistence. JSON Server uses lowdb because it requires zero setup, supports file-based storage out of the box, and allows the server to function as a simple REST API over a JSON file without external database dependencies.
How does JSON Server handle data persistence with lowdb?
JSON Server persists data by calling the write() method on the Low instance after any mutation. The Service class modifies the in-memory db.data object directly, then awaits this.#db.write() to flush changes to disk. This write operation travels through the adapter chain—Observer (emits events), NormalizedAdapter (adds schema back), and finally the file adapter (JSONFile or DataFile) which performs the actual disk I/O.
What is the NormalizedAdapter in JSON Server?
The NormalizedAdapter is a custom wrapper around lowdb’s native adapters, defined in src/adapters/normalized-adapter.ts. It enforces JSON Server’s data conventions by stripping the $schema property on read, converting numeric IDs to strings, and auto-generating missing IDs using randomId(). This ensures that the in-memory data structure always conforms to the expected schema before being processed by the Service layer.
How does JSON Server detect file changes when using lowdb?
JSON Server detects file changes through the Observer adapter (src/adapters/observer.ts), which wraps the underlying lowdb adapter and injects lifecycle hooks. The CLI in src/bin.ts attaches callbacks to onReadStart, onReadEnd, onWriteStart, and onWriteEnd. These hooks allow the server to compare endpoint states before and after reads, log changes to the console, and set flags to prevent race conditions during the write cycle.
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 →