Thin MongoDB ODM built for Standard Schema
mongodb zod deno

readme

+9 -10
README.md
···
# **Nozzle**
-
A lightweight, type-safe ODM for MongoDB in TypeScript — inspired by
-
[Drizzle ORM](https://orm.drizzle.team/) and built for developers who value
-
simplicity, transparency, and strong typings.
-
> **Note:** Nozzle DB requires MongoDB **4.2 or newer** and works best with the
> latest stable MongoDB server (6.x or newer) and the official
> [mongodb](https://www.npmjs.com/package/mongodb) Node.js driver (v6+).
## ✨ Features
-
- **Schema-first:** Define and validate collections using
-
[Zod](https://zod.dev/).
- **Type-safe operations:** Auto-complete and strict typings for `insert`,
`find`, `update`, and `delete`.
- **Minimal & modular:** No decorators or magic. Just clean, composable APIs.
-
- **Developer-friendly DX:** Great TypeScript support and IDE integration.
- **Built on MongoDB native driver:** Zero overhead with full control.
---
···
## 🚀 Quick Start
### 1. Define a schema
```ts
// src/schemas/user.ts
import { z } from "zod";
-
import { defineModel } from "@nozzle/nozzle";
export const userSchema = z.object({
name: z.string(),
···
disconnect,
InferModel,
InsertType,
-
MongoModel,
} from "@nozzle/nozzle";
import { userSchema } from "./schemas/user";
import { ObjectId } from "mongodb"; // v6+ driver recommended
···
async function main() {
// Use the latest connection string format and options
await connect("mongodb://localhost:27017", "your_database_name");
-
const UserModel = new MongoModel("users", userSchema);
// Your operations go here
···
# **Nozzle**
+
A lightweight, type-safe ODM for MongoDB in TypeScript
+
> **Note:** Nozzle requires MongoDB **4.2 or newer** and works best with the
> latest stable MongoDB server (6.x or newer) and the official
> [mongodb](https://www.npmjs.com/package/mongodb) Node.js driver (v6+).
## ✨ Features
+
- **Schema-first:** Define and validate collections using any schema validator
+
that supports [Standard Schema](https://standardschema.dev).
- **Type-safe operations:** Auto-complete and strict typings for `insert`,
`find`, `update`, and `delete`.
- **Minimal & modular:** No decorators or magic. Just clean, composable APIs.
- **Built on MongoDB native driver:** Zero overhead with full control.
---
···
## 🚀 Quick Start
+
Examples below use Zod but any schema validator that supports
+
[Standard Schema](https://standardschema.dev) will work.
+
### 1. Define a schema
```ts
// src/schemas/user.ts
import { z } from "zod";
export const userSchema = z.object({
name: z.string(),
···
disconnect,
InferModel,
InsertType,
+
Model,
} from "@nozzle/nozzle";
import { userSchema } from "./schemas/user";
import { ObjectId } from "mongodb"; // v6+ driver recommended
···
async function main() {
// Use the latest connection string format and options
await connect("mongodb://localhost:27017", "your_database_name");
+
const UserModel = new Model("users", userSchema);
// Your operations go here
-2
deno.json
···
"exports": "./mod.ts",
"license": "MIT",
"tasks": {
-
"build": "tsc",
"test:mock": "deno test tests/mock_test.ts",
"test:watch": "deno run -A scripts/test.ts --mock --watch"
},
"imports": {
"@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0",
-
"zod": "jsr:@zod/zod@^4.0.17",
"mongodb": "npm:mongodb@^6.18.0"
}
}
···
"exports": "./mod.ts",
"license": "MIT",
"tasks": {
"test:mock": "deno test tests/mock_test.ts",
"test:watch": "deno run -A scripts/test.ts --mock --watch"
},
"imports": {
"@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0",
"mongodb": "npm:mongodb@^6.18.0"
}
}
-5
deno.lock
···
"jsr:@std/internal@^1.0.10": "1.0.10",
"jsr:@std/internal@^1.0.6": "1.0.10",
"jsr:@std/testing@*": "1.0.15",
-
"jsr:@zod/zod@^4.0.17": "4.0.17",
"npm:@types/node@*": "22.15.15",
"npm:mongodb@^6.18.0": "6.18.0"
},
···
"jsr:@std/assert@^1.0.13",
"jsr:@std/internal@^1.0.10"
]
-
},
-
"@zod/zod@4.0.17": {
-
"integrity": "4d9be90a1a3c16e09dad7ce25986379d7ab8ed5f5f843288509af6bf8def525f"
}
},
"npm": {
···
"workspace": {
"dependencies": [
"jsr:@standard-schema/spec@1",
-
"jsr:@zod/zod@^4.0.17",
"npm:mongodb@^6.18.0"
]
}
···
"jsr:@std/internal@^1.0.10": "1.0.10",
"jsr:@std/internal@^1.0.6": "1.0.10",
"jsr:@std/testing@*": "1.0.15",
"npm:@types/node@*": "22.15.15",
"npm:mongodb@^6.18.0": "6.18.0"
},
···
"jsr:@std/assert@^1.0.13",
"jsr:@std/internal@^1.0.10"
]
}
},
"npm": {
···
"workspace": {
"dependencies": [
"jsr:@standard-schema/spec@1",
"npm:mongodb@^6.18.0"
]
}
+1 -1
examples/user.ts
···
-
import { z } from "zod";
import { ObjectId } from "mongodb";
import {
connect,
···
+
import { z } from "jsr:@zod/zod";
import { ObjectId } from "mongodb";
import {
connect,
+43 -69
model.ts
···
import { ObjectId } from "mongodb";
import { getDb } from "./client.ts";
-
export class Model<T extends StandardSchemaV1<unknown, Document>> {
-
private collection: Collection<StandardSchemaV1.InferOutput<T>>;
private schema: T;
constructor(collectionName: string, schema: T) {
-
this.collection = getDb().collection<
-
StandardSchemaV1.InferOutput<T> & Document
-
>(
-
collectionName,
-
);
this.schema = schema;
}
-
async insertOne(
-
data: StandardSchemaV1.InferInput<T>,
-
): Promise<InsertOneResult<StandardSchemaV1.InferOutput<T>>> {
-
const result = this.schema["~standard"].validate(data);
-
if (result instanceof Promise) {
-
throw new Error("Async validation not supported");
-
}
-
if (result.issues) {
-
throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
-
}
return await this.collection.insertOne(
-
result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>,
);
}
-
async insertMany(
-
data: StandardSchemaV1.InferInput<T>[],
-
): Promise<InsertManyResult<StandardSchemaV1.InferOutput<T>>> {
-
const validatedData = data.map((item) => {
-
const result = this.schema["~standard"].validate(item);
-
if (result instanceof Promise) {
-
throw new Error("Async validation not supported");
-
}
-
if (result.issues) {
-
throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
-
}
-
return result.value;
-
});
return await this.collection.insertMany(
-
validatedData as OptionalUnlessRequiredId<
-
StandardSchemaV1.InferOutput<T>
-
>[],
);
}
-
async find(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
-
): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> {
return await this.collection.find(query).toArray();
}
-
async findOne(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
-
): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> {
return await this.collection.findOne(query);
}
-
async findById(
-
id: string | ObjectId,
-
): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> {
const objectId = typeof id === "string" ? new ObjectId(id) : id;
-
return await this.findOne(
-
{ _id: objectId } as Filter<StandardSchemaV1.InferOutput<T>>,
-
);
}
async update(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
-
data: Partial<StandardSchemaV1.InferOutput<T>>,
): Promise<UpdateResult> {
return await this.collection.updateMany(query, { $set: data });
}
async updateOne(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
-
data: Partial<StandardSchemaV1.InferOutput<T>>,
): Promise<UpdateResult> {
return await this.collection.updateOne(query, { $set: data });
}
async replaceOne(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
-
data: StandardSchemaV1.InferInput<T>,
): Promise<UpdateResult> {
-
const result = this.schema["~standard"].validate(data);
-
if (result instanceof Promise) {
-
throw new Error("Async validation not supported");
-
}
-
if (result.issues) {
-
throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
-
}
return await this.collection.replaceOne(
query,
-
result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>,
);
}
-
async delete(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
-
): Promise<DeleteResult> {
return await this.collection.deleteMany(query);
}
-
async deleteOne(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
-
): Promise<DeleteResult> {
return await this.collection.deleteOne(query);
}
-
async count(query: Filter<StandardSchemaV1.InferOutput<T>>): Promise<number> {
return await this.collection.countDocuments(query);
}
···
// Pagination support for find
async findPaginated(
-
query: Filter<StandardSchemaV1.InferOutput<T>>,
options: { skip?: number; limit?: number; sort?: Document } = {},
-
): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> {
return await this.collection
.find(query)
.skip(options.skip ?? 0)
···
import { ObjectId } from "mongodb";
import { getDb } from "./client.ts";
+
// Type alias for cleaner code
+
type Schema = StandardSchemaV1<unknown, Document>;
+
type Infer<T extends Schema> = StandardSchemaV1.InferOutput<T>;
+
type Input<T extends Schema> = StandardSchemaV1.InferInput<T>;
+
+
// Helper function to make StandardSchemaV1 validation as simple as Zod's parse()
+
function parse<T extends Schema>(schema: T, data: unknown): Infer<T> {
+
const result = schema["~standard"].validate(data);
+
if (result instanceof Promise) {
+
throw new Error("Async validation not supported");
+
}
+
if (result.issues) {
+
throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
+
}
+
return result.value;
+
}
+
+
export class Model<T extends Schema> {
+
private collection: Collection<Infer<T>>;
private schema: T;
constructor(collectionName: string, schema: T) {
+
this.collection = getDb().collection<Infer<T> & Document>(collectionName);
this.schema = schema;
}
+
async insertOne(data: Input<T>): Promise<InsertOneResult<Infer<T>>> {
+
const validatedData = parse(this.schema, data);
return await this.collection.insertOne(
+
validatedData as OptionalUnlessRequiredId<Infer<T>>,
);
}
+
async insertMany(data: Input<T>[]): Promise<InsertManyResult<Infer<T>>> {
+
const validatedData = data.map((item) => parse(this.schema, item));
return await this.collection.insertMany(
+
validatedData as OptionalUnlessRequiredId<Infer<T>>[],
);
}
+
async find(query: Filter<Infer<T>>): Promise<(WithId<Infer<T>>)[]> {
return await this.collection.find(query).toArray();
}
+
async findOne(query: Filter<Infer<T>>): Promise<WithId<Infer<T>> | null> {
return await this.collection.findOne(query);
}
+
async findById(id: string | ObjectId): Promise<WithId<Infer<T>> | null> {
const objectId = typeof id === "string" ? new ObjectId(id) : id;
+
return await this.findOne({ _id: objectId } as Filter<Infer<T>>);
}
async update(
+
query: Filter<Infer<T>>,
+
data: Partial<Infer<T>>,
): Promise<UpdateResult> {
return await this.collection.updateMany(query, { $set: data });
}
async updateOne(
+
query: Filter<Infer<T>>,
+
data: Partial<Infer<T>>,
): Promise<UpdateResult> {
return await this.collection.updateOne(query, { $set: data });
}
async replaceOne(
+
query: Filter<Infer<T>>,
+
data: Input<T>,
): Promise<UpdateResult> {
+
const validatedData = parse(this.schema, data);
return await this.collection.replaceOne(
query,
+
validatedData as OptionalUnlessRequiredId<Infer<T>>,
);
}
+
async delete(query: Filter<Infer<T>>): Promise<DeleteResult> {
return await this.collection.deleteMany(query);
}
+
async deleteOne(query: Filter<Infer<T>>): Promise<DeleteResult> {
return await this.collection.deleteOne(query);
}
+
async count(query: Filter<Infer<T>>): Promise<number> {
return await this.collection.countDocuments(query);
}
···
// Pagination support for find
async findPaginated(
+
query: Filter<Infer<T>>,
options: { skip?: number; limit?: number; sort?: Document } = {},
+
): Promise<(WithId<Infer<T>>)[]> {
return await this.collection
.find(query)
.skip(options.skip ?? 0)
+9 -12
schema.ts
···
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { ObjectId } from "mongodb";
-
export type InferModel<
-
T extends StandardSchemaV1<unknown, Record<string, unknown>>,
-
> =
-
& StandardSchemaV1.InferOutput<T>
-
& {
-
_id?: ObjectId;
-
};
-
export type InsertType<
-
T extends StandardSchemaV1<unknown, Record<string, unknown>>,
-
> =
-
& Omit<StandardSchemaV1.InferOutput<T>, "createdAt">
-
& { createdAt?: Date };
···
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { ObjectId } from "mongodb";
+
type Schema = StandardSchemaV1<unknown, Record<string, unknown>>;
+
type Infer<T extends Schema> = StandardSchemaV1.InferOutput<T>;
+
+
export type InferModel<T extends Schema> = Infer<T> & {
+
_id?: ObjectId;
+
};
+
export type InsertType<T extends Schema> = Omit<Infer<T>, "createdAt"> & {
+
createdAt?: Date;
+
};
+1 -1
tests/main_test.ts
···
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
-
import { z } from "zod";
import { connect, disconnect, type InsertType, Model } from "../mod.ts";
import { ObjectId } from "mongodb";
···
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
+
import { z } from "jsr:@zod/zod";
import { connect, disconnect, type InsertType, Model } from "../mod.ts";
import { ObjectId } from "mongodb";
+1 -1
tests/mock_test.ts
···
import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd";
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
-
import { z } from "zod";
// Mock implementation for demonstration
class MockModel<T> {
···
import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd";
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
+
import { z } from "jsr:@zod/zod";
// Mock implementation for demonstration
class MockModel<T> {