A lightweight JSON database for TypeScript with schema validation and indexed search. Zero dependencies. Works in-memory or persisted to disk.
- Written in TypeScript with full type inference
- Schema validation with
string,number,boolean,objecttypes - Fast indexed search with prefix, suffix, contains, and range queries
- Sorting by indexed fields (ascending/descending) without client-side sort
- In-memory or file-based persistence
- Zero dependencies, Node.js 24+
npm install js-idbimport { createDB } from "js-idb"; const db = createDB({ collections: { users: { schema: { name: { type: "string", index: true, indexSetting: { ignoreCase: true } }, age: { type: "number", index: true }, active: { type: "boolean", default: true }, meta: { type: "object" }, }, }, }, }); db.users.add({ name: "Josef", age: 30, active: true, meta: { role: "admin" } }); db.users.find({ name: "josef" }); // case-insensitive matchEach collection requires a schema. Fields support four types:
| Type | JS type | Indexable | Notes |
|---|---|---|---|
string | string | Yes | Supports ignoreCase index setting |
number | number | Yes | NaN is rejected |
boolean | boolean | Yes | |
object | Record<string, unknown> | No | Arbitrary data, nesting allowed, no type checking on contents |
{ type: "string", // required — field type index: true, // optional — enable search via find() indexSetting: { // optional — only for indexed string fields ignoreCase: true, }, default: "", // optional — applied when field is omitted on add }- All fields are required on
addunless they have adefault defaultvalues are validated against the field type at database creationupdatealways accepts partial records
Creates a database instance.
const db = createDB({ path: "./data", // optional — omit for in-memory only collections: { users: { schema: { /* ... */ } }, }, });With file persistence, each collection is stored as <name>.data.json and <name>.meta.json. Indexes are rebuilt from data on load.
Inserts a single record. Returns the record with an auto-generated _id.
const doc = db.users.add({ name: "Josef", age: 30, active: true, meta: {} }); // doc._id — auto-generated unique IDInserts multiple records in a single batch (one write operation).
const docs = db.users.addMany([ { name: "Karel", age: 25, active: true, meta: {} }, { name: "Anna", age: 35, active: false, meta: {} }, ]);Retrieves a single record by ID.
const doc = db.users.get("some-id");Returns all records in the collection. Supports optional sorting.
const docs = db.users.all(); const sorted = db.users.all({ sort: 'age' }); // ascending const desc = db.users.all({ sort: '-age' }); // descendingSearches indexed fields. All queried fields must have index: true. Multiple fields are intersected (AND). Supports optional sorting.
// String queries db.users.find({ name: "josef" }); // exact match db.users.find({ name: "jos%" }); // prefix db.users.find({ name: "%sef" }); // suffix db.users.find({ name: "%ose%" }); // contains // Number queries db.users.find({ age: "30" }); // exact db.users.find({ age: ">20" }); // greater than db.users.find({ age: ">=20" }); // greater than or equal db.users.find({ age: "<30" }); // less than db.users.find({ age: "<=30" }); // less than or equal // Boolean queries db.users.find({ active: "true" }); // Compound (intersection) db.users.find({ name: "jos%", age: "<=30" }); // With sorting db.users.find({ age: ">20" }, { sort: "name" }); // results sorted by name db.users.find({ active: "true" }, { sort: "-age" }); // sorted by age descendingUpdates specific fields on an existing record. Accepts a partial record.
const updated = db.users.update(doc._id, { name: "Josef II" });Deletes a record by ID.
db.users.remove(doc._id);Removes all records from the collection.
db.users.clear();Returns the number of records in the collection.
db.users.count; // 42Access a collection by name (useful for dynamic access).
const col = db.collection("users");Types are inferred from the schema automatically:
const db = createDB({ collections: { users: { schema: { name: { type: "string" }, age: { type: "number" }, }, }, }, }); const doc = db.users.add({ name: "Josef", age: 30 }); doc.name; // string doc.age; // number doc._id; // stringFor more control (e.g. making fields with defaults optional), provide your own interface:
interface User { name: string; age: number; active?: boolean; // optional — schema has default: true } const db = createDB<{ users: User }>({ collections: { users: { schema: { name: { type: "string", index: true }, age: { type: "number" }, active: { type: "boolean", default: true }, }, }, }, }); db.users.add({ name: "Josef", age: 30 }); // active is optional db.users.update(id, { age: 31 }); // Partial<User> const doc = db.users.get(id); // User & { _id: string } | undefinedBoth all() and find() accept an optional { sort } parameter. Prefix the field name with - for descending order.
db.users.all({ sort: 'age' }); // ascending by age db.users.all({ sort: '-name' }); // descending by name db.users.find({ active: 'true' }, { sort: 'name' }); // filtered + sortedOnly indexed fields can be used for sorting. Since indexes are stored as sorted arrays, sorting is O(n) — a linear scan of pre-sorted data — instead of the O(n log n) required by a client-side sort.
Indexed search uses sorted data structures, not full scans.
- All data and indexes live in RAM — fastest possible reads and writes
- Data is lost when the process exits
- Best for temporary data, caches, or browser environments
- Data and indexes live on disk — nothing is held in memory
- Every operation (
get,find,add,update,remove) reads from and writes to disk addManybatches into a single write — significantly faster than individualaddcalls- Indexes are persisted in the meta file and used directly on search — no rebuilding on startup
- On startup, the stored schema is validated against the provided schema — if they differ, files are regenerated (stale data is wiped)
- Best for small to medium datasets that need to survive restarts
MIT