js-idb
v1.0.1
Published
Simple JSON database with fast search over data via indexes and type checking
Downloads
14
Maintainers
Readme
js-idb
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+
Install
npm install js-idbQuick start
import { 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 matchSchema
Each 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 |
Field options
{
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
API
createDB(options)
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.
collection.add(record): Document
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 IDcollection.addMany(records): Document[]
Inserts 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: {} },
]);collection.get(id): Document | undefined
Retrieves a single record by ID.
const doc = db.users.get("some-id");collection.all(options?): Document[]
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' }); // descendingcollection.find(query, options?): Document[]
Searches 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 descendingcollection.update(id, partial): Document
Updates specific fields on an existing record. Accepts a partial record.
const updated = db.users.update(doc._id, { name: "Josef II" });collection.remove(id): void
Deletes a record by ID.
db.users.remove(doc._id);collection.clear(): void
Removes all records from the collection.
db.users.clear();collection.count: number
Returns the number of records in the collection.
db.users.count; // 42db.collection(name)
Access a collection by name (useful for dynamic access).
const col = db.collection("users");TypeScript
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 } | undefinedSorting
Both 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.
Performance
Indexed search uses sorted data structures, not full scans.
In-memory mode
- 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
File persistence
- 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
License
MIT
