Get compile-time safety for all your MongoDB CRUD operations, plus optional runtime validation using Zod. Runtime checks are enabled by default for extra safety, but can be disabled for zero runtime overhead.
- Type-safe CRUD: Compile-time type checking for all MongoDB operations
- Zod integration: Use your Zod schemas as the single source of truth
- With runtime validation: Enforces schema correctness at both compile-time and runtime using Zod.
- Familiar API: Mirrors the official MongoDB Node.js driver
npm install mongo-type-safe zod mongodbdb.ts
import { MongoClient } from "mongodb";
export const client = new MongoClient("mongodb://localhost:27017");index.ts (entrypoint of your app)
import { client } from "./db";
await client.connect(); // Ensure connection before importing models
export const db = client.db("test-db");
// ...rest of your appuserModel.ts
import { z } from "zod";
import { createSafeCollection } from "mongo-type-safe";
import { db } from "./index"; // or "./db" if you export db there
export const userSchema = z.object({
name: z.string(),
age: z.number().optional(),
email: z.string().email()
});
export const safeUsers = createSafeCollection(db.collection("users"), userSchema);Anywhere in your app:
import { safeUsers } from "./userModel";
await safeUsers.insertOne({ name: "Marko", email: "marko@example.com" });
const user = await safeUsers.findOne({ name: "Marko" });Note: This pattern ensures that your models always have a ready-to-use, type-safe collection. Just make sure the db connection is established before any model is imported.
userModel.ts
import { z } from "zod";
export const userSchema = z.object({
name: z.string(),
age: z.number().optional(),
email: z.string().email()
});In your route/service/handler:
import { MongoClient } from "mongodb";
import { createSafeCollection } from "mongo-type-safe";
import { userSchema } from "./userModel";
async function main() {
const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("test-db");
// Create the type-safe collection on demand
const safeUsers = createSafeCollection(db.collection("users"), userSchema);
await safeUsers.insertOne({ name: "Marko", email: "marko@example.com" });
const user = await safeUsers.findOne({ name: "Marko" });
await safeUsers.updateOne({ name: "Marko" }, { $set: { age: 30 } });
await safeUsers.deleteOne({ name: "Marko" });
client.close();
}
main();userModel.ts
import { z } from "zod";
import { createSafeCollection } from "mongo-type-safe";
import { db } from "./db"; // your global db instance
export const userSchema = z.object({
name: z.string(),
age: z.number().optional(),
email: z.string().email()
});
export const safeUsers = createSafeCollection(db.collection("users"), userSchema);Anywhere in your app:
import { safeUsers } from "./userModel";
await safeUsers.insertOne({ name: "Marko", email: "marko@example.com" });
const user = await safeUsers.findOne({ name: "Marko" });By default, createSafeCollection validates all filters and update objects at runtime using the Zod schema you provide. This helps catch bugs early and keeps your data access safe and predictable.
Runtime validation ensures only valid filter/update structures are allowed:
const users = createSafeCollection(collection, userSchema);
await users.insertOne({ name: "Marko", age: 23 }); // ✅ OK
// 👇 TypeScript would normally catch this at compile time,
// but we use `as any` to simulate invalid data at runtime.
await users.insertOne({ name: 23 as any, age: "Marko" as any }); // ❌ Throws: Invalid document: expected string, received numberNOTE: as any is used to bypass TypeScript so we can demonstrate runtime validation. Without it, TypeScript would block the error before runtime.
const users = createSafeCollection(collection, userSchema, { strict: false });
await users.insertOne({ name: "Marko", age: 23 }); // ✅ allowed
// 👇 Still compiles because we forced the types,
// but now no runtime validation is applied either.
await users.insertOne({ name: 23 as any, age: "Marko" as any }); // ✅ no error thrownAll methods are type-safe and mirror the MongoDB driver:
insertOne(doc, options?)insertMany(docs, options?)findOne(filter, options?)find(filter, options?)updateOne(filter, update, options?)updateMany(filter, update, options?)deleteOne(filter, options?)deleteMany(filter, options?)replaceOne(filter, newDocument, options?)findOneAndUpdate(filter, update, options?)findOneAndReplace(filter, newDocument, options?)findOneAndDelete(filter, options?)countDocuments(filter?, options?)estimatedDocumentCount(options?)distinct(key, filter?)aggregate(pipeline, options?)
See the source for full method signatures and JSDoc documentation.
- All filters, updates, and documents are checked at compile time against your Zod schema.
- Invalid field names or types will cause TypeScript errors before you run your code.
- Use your Zod schemas to define the shape of your documents.
- The wrapper automatically infers TypeScript types from your Zod schema.
MIT