npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

js-mongo-orm

v1.0.0

Published

A tiny, fast, type-safe active-record ORM for MongoDB with transparent dirty tracking and partial updates.

Readme

MongoORM

A tiny, fast, type-safe active-record ORM for MongoDB.

Define a class, mutate its fields, call save() — only what actually changed is written.

npm version license types tests


MongoORM is a minimal active-record layer over the official mongodb driver. It gives you ergonomic models without a schema compiler, a query builder, or a heavyweight runtime — just plain classes whose changes are tracked transparently and persisted as surgical partial updates.

class User extends Model {
    name!: string;
    profile!: { bio: string; city: string };
}

const user = await User.findOrFail(id);
user.profile.city = "Yerevan";          // deep mutation, tracked automatically
await user.save();                       // → { $set: { "profile.city": "Yerevan" } }

That last line sends only the changed nested path to MongoDB — not the whole document.

Contents

Features

  • 🪶 Tiny. A handful of files, one dependency (mongodb). No schema layer, no codegen.
  • Transparent dirty tracking. Read and mutate fields like a normal object — including nested ones. The model knows exactly what changed.
  • 🎯 Surgical partial updates. save() emits precise $set / $unset operations down to nested dot-paths, never a blind full-document overwrite.
  • Built for throughput. Lazy change-snapshots keep reads cheap; saveMany() collapses a write loop into a single bulkWrite (~24× faster on large batches — see Performance).
  • 🔒 Type-safe. Written in TypeScript, ships .d.ts. Your models are real classes with real types.
  • 🧩 Familiar API. find, findOrFail, findMany, save, delete, serialize — if you've used Eloquent or Active Record, you already know it.
  • 🛟 Guarded fields & hooks. Hide sensitive fields from serialization; run beforeSave() logic automatically.

Requirements

  • Node.js ≥ 16
  • MongoDB server reachable via a connection string
  • Peer/runtime dependency: mongodb ^5

Installation

npm install js-mongo-orm mongodb

Quick start

1. Connect

import { DB } from "js-mongo-orm";

await DB.createDBConnection({
    name: "main",               // logical name your models reference
    db_name: "app",             // database on the server
    type: "MongoDB",
    connectionString: process.env.MONGO_URL!,
});

Single database? The first connection you register automatically becomes the default, so your models can skip $db entirely. With several connections, mark one with default: true (or call DB.setDefault(name)) to choose it.

2. Define a model

import { Model, ObjectId } from "js-mongo-orm";

class User extends Model {
    protected $db = "main";            // optional — omit to use the default connection
    protected $guarded = ["password"]; // never serialized / enumerated

    _id!: ObjectId;
    name!: string;
    email!: string;
    password!: string;
}

The collection name is inferred from the class name (Userusers, UserModeluser_models). Override it with protected $table = "people" when you need to.

The ! (definite assignment) tells TypeScript these fields are populated at runtime by the ORM, not in a constructor.

3. Create, read, update, delete

// Create
const user = new User();
user.name = "Ada";
user.email = "[email protected]";
await user.save();              // inserts; user._id is now populated

// Read
const byId = await User.find(user._id);            // User | undefined
const orFail = await User.findOrFail(user._id);    // User | throws
const admins = await User.findMany({ role: "admin" });

// Update (partial — only changed fields are written)
orFail.name = "Ada Lovelace";
await orFail.save();           // → { $set: { name: "Ada Lovelace" } }

// Delete (soft delete — stamps __deletedAt)
await orFail.delete();

4. Nested updates just work

const user = await User.findOrFail(id);
user.settings.notifications.email = false;
delete user.settings.legacyFlag;
await user.save();
// → { $set: { "settings.notifications.email": false },
//     $unset: { "settings.legacyFlag": true } }

5. Batch writes with saveMany

const users = await User.findMany({ active: true });
users.forEach(u => (u.lastSeenAt = Date.now()));

await User.saveMany(users);     // one bulkWrite() instead of N round-trips

saveMany groups inserts and updates per collection, issues a single bulkWrite / insertMany, backfills generated _ids, and re-baselines every model for further edits.

6. Lifecycle hooks

Define beforeSave() on a model and it runs automatically before every save() and saveMany():

class Post extends Model {
    updatedAt!: number;
    beforeSave() { this.updatedAt = Date.now(); }
}

Why MongoORM?

| | MongoORM | Hand-written driver code | Heavy ODM | | --- | --- | --- | --- | | Partial nested updates | Automatic | Manual $set paths | Varies | | Dirty tracking | Built-in | DIY | Yes | | Bundle / runtime weight | Minimal | None | Large | | Schema layer required | No | No | Usually | | Batch helper | saveMany | DIY | Varies | | Type-safe models | Yes | Partial | Yes |

If you love the raw driver but are tired of writing $set paths by hand and re-implementing change tracking on every project, MongoORM is the thin layer that removes that boilerplate — and nothing else.

Performance

Dirty tracking is implemented so that the common paths stay cheap:

  • Reads are free. The change-baseline is captured lazily, only when a model is first mutated. Hydrating documents you only read never pays for a deep clone.
  • Writes are batched. saveMany() turns an N-document write loop into a single network round-trip.

Measured on a local MongoDB, N = 20,000 documents (npm run bench):

| Operation | Before | After | Speedup | | --- | ---: | ---: | ---: | | Hydrate 20k models (construction CPU) | 129 ms | 49 ms | ~2.6× | | Persist 2k changed docs | 1751 ms (save loop) | 73 ms (saveMany) | ~24× | | Read a field through a model | — | ~0.2 µs | negligible |

Numbers vary with hardware and network latency. The saveMany advantage grows on remote databases, where round-trip latency dominates.

How it works

Each model instance is wrapped in a Proxy. The handler:

  1. Intercepts writes and records them, while letting real getters/setters and class fields behave normally.
  2. Captures a pristine snapshot lazily — the first time you mutate a field (or read a nested object that could be mutated in place), so read-only models stay allocation-light.
  3. On save(), deep-diffs the working copy against that snapshot and translates the result into precise Mongo $set / $unset operands, flattening nested objects into dot-paths.

The result: you write ordinary object code, and the database sees the smallest correct update.

API reference

DB — connection registry

| Member | Description | | --- | --- | | DB.createDBConnection(config) | Open and register a named connection. | | DB.db(name?) | Resolve a Db by name, falling back to the default connection. | | DB.setDefault(name) | Choose the default connection for models without $db. | | DB.close(name?) | Close one connection, or all of them. | | DB.connections / DB.dbs | Raw MongoClient / Db registries, keyed by name. |

ConnectionConfig:

| Field | Type | Description | | --- | --- | --- | | name | string | Logical name models reference via $db. | | db_name | string | Database name on the server. | | type | "MongoDB" | Driver to use. | | connectionString | string | Standard MongoDB connection string. | | default | boolean? | Make this the default connection. |

Model — instance methods

| Method | Description | | --- | --- | | save(needInsert?, bigUpdate?, sortProperties?) | Insert or partially update the document. | | delete() | Soft-delete (stamps __deletedAt). | | changes() | The sparse change tree since load / last save (undefined if clean). | | serialize() | Plain object view with $guarded fields removed. |

save() options:

| Argument | Default | Effect | | --- | --- | --- | | needInsert | false | Force an insert even when a primary key is present. | | bigUpdate | false | Replace whole top-level fields instead of computing nested dot-paths. | | sortProperties | false | Sort keys before writing (deterministic output). |

Model — static methods

| Method | Description | | --- | --- | | find(filterOrKey) | One document → Model \| undefined. Accepts an ObjectId or a filter. | | findOrFail(filterOrKey) | One document → Model, or throws if missing. | | findMany(filter?, options?) | Array of models. Pass true as the 3rd argument to get the raw Mongo cursor instead. | | saveMany(models, options?) | Batch insert/update in one round-trip per collection. |

saveMany() options:

| Option | Default | Effect | | --- | --- | --- | | bigUpdate | false | Same as save()'s bigUpdate, applied per model. | | sortProperties | false | Sort each document's keys before writing. | | ordered | false | Run as an ordered bulk write (slower, fail-fast). |

Model configuration (protected fields)

| Field | Default | Purpose | | --- | --- | --- | | $db | "" | Connection name to use. Empty ⇒ the default connection. | | $table | derived | Collection name (defaults to the pluralized class name). | | $primaryKey | "_id" | Field used for lookups and updates. | | $guarded | ["password"] | Fields hidden from serialize() and key enumeration. |

Helpers

| Export | Description | | --- | --- | | generateTableName(className) | Class name → collection name (UserModeluser_models). | | pluralize(word, singularize?) | The English inflector used for collection names. | | ObjectId | Re-exported from the mongodb driver. |

Good to know

A few behaviors worth keeping in mind:

  • Soft delete is opt-in on reads. delete() only stamps __deletedAt; find / findMany do not filter deleted documents automatically. Add { __deletedAt: { $exists: false } } to your queries (or a $where helper) if you want them hidden.
  • undefined does not remove a field. Assigning model.x = undefined is a no-op for deletion and logs a warning. Use delete model.x to emit a $unset.
  • Snapshots clone via JSON. The dirty-tracking baseline is a structured (BSON-friendly) deep clone, so values that don't survive a JSON round-trip (functions, class instances with custom prototypes) aren't tracked field-by-field. Plain documents, dates, numbers, arrays, and nested objects all work as expected.
  • $guarded defaults to ["password"]. Override it per model — set it to [] to serialize everything.

Testing

The suite runs against a real MongoDB. Point it at any instance via MONGO_URL:

# spin up a throwaway MongoDB
docker run -d -p 27017:27017 --name mongo-orm-test mongo:6

MONGO_URL="mongodb://localhost:27017/mongoorm_test" npm test

No credentials are committed — the connection string comes entirely from the environment.

Development

npm run build      # compile src/ → bin/
npm test           # build + run jest
npm run bench      # micro-benchmarks (needs MONGO_URL)

Contributions are welcome — see CONTRIBUTING.md. Notable changes are recorded in CHANGELOG.md.

License

MIT © Alexander Asrumyan