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

@mauroandre/zodmongo

v0.0.5

Published

Lightweight MongoDB ODM with Zod validation. TypeScript-first, no Mongoose.

Downloads

626

Readme

ZodMongo

Lightweight MongoDB ODM powered by Zod schemas. TypeScript-first, built on the native MongoDB driver — no Mongoose.

Why ZodMongo?

  • No Mongoose — uses the native MongoDB driver, zero overhead
  • Zod-native — define schemas with Zod, not a proprietary format
  • TypeScript-first — types inferred directly from your Zod schemas
  • Tiny — ~300 lines of code, only mongodb and zod as dependencies
  • Transparent id/ObjectId — work with id (string) in your app, _id (ObjectId) in MongoDB
  • Automatic timestampscreatedAt and updatedAt managed by the ODM
  • Built-in pagination — via aggregation pipeline with $facet
  • Full aggregation pipelinefindMany accepts a complete pipeline, not just match filters
  • Relations & Snapshots — declare references to other collections with automatic $lookup generation

Install

npm install @mauroandre/zodmongo

Quick Start

import { connect, close, save, findMany, deleteMany } from "@mauroandre/zodmongo";
import { dbSchema } from "@mauroandre/zodmongo";
import { z } from "zod/v4";

// Connect
await connect("mongodb://localhost:27017", "mydb");

// Define a schema
const userSchema = dbSchema({
    name: z.string(),
    email: z.string().email(),
});
type User = z.infer<typeof userSchema>;

// Insert
const user = userSchema.parse({ name: "Mauro", email: "[email protected]" });
await save("users", user);
console.log(user.id); // ObjectId string, auto-assigned

// Update
user.name = "Mauro André";
await save("users", user);

// Find
const users = await findMany<User>("users", { name: "Mauro André" });
const all = await findMany<User>("users");

// Delete
await deleteMany("users", { email: "[email protected]" });

// Close
await close();

Schemas

dbSchema(shape)

Creates a schema that extends the base model with id, createdAt, and updatedAt. This is the primary way to define your models.

import { dbSchema } from "@mauroandre/zodmongo";
import { z } from "zod/v4";

const postSchema = dbSchema({
    title: z.string(),
    body: z.string(),
    published: z.boolean().default(false),
});
type Post = z.infer<typeof postSchema>;
// { id: string | null, createdAt: Date | null, updatedAt: Date | null, title: string, body: string, published: boolean }

embeddedSchema(shape)

Creates a schema without the base model fields (id, createdAt, updatedAt). Use for nested objects that don't need their own identity.

import { embeddedSchema } from "@mauroandre/zodmongo";
import { z } from "zod/v4";

const addressSchema = embeddedSchema({
    street: z.string(),
    city: z.string(),
    zip: z.string(),
});

const userSchema = dbSchema({
    name: z.string(),
    address: addressSchema,
});

dbModelSchema and idSchema

Low-level schemas if you need to extend manually:

import { dbModelSchema, idSchema } from "@mauroandre/zodmongo/schema";

const customSchema = dbModelSchema.extend({
    name: z.string(),
});

// idSchema validates a string as a valid ObjectId
idSchema.parse("507f1f77bcf86cd799439011"); // ok
idSchema.parse("invalid"); // throws

API

connect(uri, dbName)

Connects to MongoDB. Returns the Db instance.

const db = await connect("mongodb://localhost:27017", "mydb");

close()

Waits for pending promises and closes the connection.

await close();

getDb()

Returns the current Db instance. Throws if not connected.

const db = getDb();
const collection = db.collection("users");

save(collection, doc, filter?, options?)

Smart upsert — inserts if no id, updates if id exists. Automatically manages createdAt and updatedAt.

// Insert (no id)
const user = userSchema.parse({ name: "Mauro", email: "[email protected]" });
await save("users", user);
// user.id is now set to the generated ObjectId string

// Update (has id)
user.name = "Updated";
await save("users", user);

// Upsert with custom filter
await save("users", user, { email: "[email protected]" });

// Disable upsert (update only, no insert)
await save("users", user, { email: "[email protected]" }, { upsert: false });

findMany<T>(collection, matchOrPipeline?, options?)

Finds documents using a simple match object or a full aggregation pipeline. Automatically converts _id to id and ObjectIds to strings in the results.

// All documents
const all = await findMany<User>("users");

// Simple match
const admins = await findMany<User>("users", { role: "admin" });

// Find by id
const found = await findMany<User>("users", { id: "507f1f77bcf86cd799439011" });

// Full aggregation pipeline
const topAdmins = await findMany<User>("users", [
    { $match: { role: "admin" } },
    { $sort: { createdAt: -1 } },
    { $limit: 10 },
]);

Pagination

Pass { paginate: true } to get paginated results via $facet:

const page = await findMany<User>("users", {}, {
    paginate: true,
    currentPage: 1,
    docsPerPage: 20,
});

page.docs;          // User[]
page.currentPage;   // 1
page.pageQuantity;  // total pages
page.docsQuantity;  // total documents

deleteMany(collection, filter)

Deletes documents matching the filter. Automatically converts id to _id.

await deleteMany("users", { email: "[email protected]" });
await deleteMany("users", { id: "507f1f77bcf86cd799439011" });

Relations

Declare references between collections. The ODM generates $lookup pipelines automatically.

relation(schema, config)

Marks a field as a reference to another collection.

const companySchema = dbSchema({ name: z.string() });

const userSchema = dbSchema({
    name: z.string(),
    company: relation(companySchema, { collection: "companies" }),
});

// When fetching, use getPipeline() to auto-generate $lookup stages
const pipeline = getPipeline(userSchema);
const users = await findMany<User>("users", pipeline);
// users[0].company is now the full company document, not just an ObjectId

When saving, use toSave() to convert relations back to ObjectIds:

const dataToSave = toSave(userSchema, userData);
// dataToSave.company is now an ObjectId
await save("users", dataToSave);

Array relations

const tagSchema = dbSchema({ label: z.string() });

const postSchema = dbSchema({
    title: z.string(),
    tags: z.array(relation(tagSchema, { collection: "tags" })),
});

Custom foreign field

const categorySchema = dbSchema({ slug: z.string(), name: z.string() });

const postSchema = dbSchema({
    title: z.string(),
    category: relation(categorySchema, { collection: "categories", foreignField: "slug" }),
});

Reverse relations

When a field is a populated destination (not a stored reference), use localField to match on another field of the same document:

const appSchema = dbSchema({
    quadletName: z.string(),
    policy: relation(backupPolicySchema, {
        collection: "backupPolicies",
        localField: "quadletName", // match on quadletName
        foreignField: "app",       // against backupPolicies.app
    }),
});

toSave() automatically omits reverse relation fields from the saved document.

Circular references

If two schemas reference each other (e.g. User ↔ Company), ZodMongo detects the cycle and truncates the nested pipeline — the outer lookups expand normally, but once a collection appears in the ancestor chain, the inner $lookup is generated without a nested pipeline. Sibling references to the same collection are not affected.

snapshot(schema)

Marks a field as a persisted copy. The ODM will not generate $lookup for it and will not convert it to ObjectId when saving. Useful for denormalized data you want to store as-is.

const userSchema = dbSchema({
    name: z.string(),
    company: snapshot(companySchema), // stored as a full copy, no lookup
});

getPipeline(schema)

Generates the aggregation pipeline (with $lookup, $set, $project) from a schema.

const pipeline = getPipeline(userSchema);
// Use it with findMany
const users = await findMany<User>("users", [...pipeline, { $match: { active: true } }]);

toSave(schema, data)

Parses data through the schema and converts relations to ObjectIds for saving.

const prepared = toSave(userSchema, rawData);
await save("users", prepared);

id ↔ _id ↔ ObjectId

ZodMongo automatically handles conversions between your app's id (string) and MongoDB's _id (ObjectId):

| Direction | What happens | |---|---| | Reading (MongoDB → App) | _id (ObjectId) becomes id (string), recursively | | Saving (App → MongoDB) | id (string) becomes _id (ObjectId), valid ObjectId strings are converted | | Querying | { id: "..." } becomes { _id: ObjectId("...") }, supports dot-notation (company.idcompany._id) |

Automatic Timestamps

  • createdAt — set automatically on insert ($setOnInsert), never modified on update
  • updatedAt — set on every save ($set)

Promise Tracking

Use trackPromise() to register fire-and-forget operations. close() waits for all tracked promises before disconnecting.

import { trackPromise, close } from "@mauroandre/zodmongo";

trackPromise(save("logs", logEntry));
trackPromise(save("logs", anotherEntry));

await close(); // waits for both saves to complete

Development

# Start MongoDB
docker compose up -d

# Run tests
npm test

# Type check
npm run typecheck

# Build
npm run build

License

MIT