Building Monarch: A Type-Safe ODM for MongoDB
The Problem
MongoDB is incredibly flexible, but that flexibility comes at a cost. It has zero guardrails.
- Typos in field names go unnoticed until production
- Schema changes break queries silently
- Worse of all, there's no intellisense or autocomplete
Mongoose helps, but its TypeScript support has always felt like an afterthought. I wanted something lean, modern and TypeScript-first where the schema is the source of truth for EVERYTHING.
That's why I built Monarch.
What Is MonarchORM?
Monarch is a type-safe ODM (Object-Document Mapper) for MongoDB. It takes a schema-first approach where you define your document shapes once and get full type safety everywhere:
import { createClient, createDatabase, createSchema, defineSchemas } from "monarch-orm";
import { boolean, number, string } from "monarch-orm/types";
const UserSchema = createSchema("users", {
name: string(),
email: string(),
age: number().default(10),
isVerified: boolean(),
});
const schemas = defineSchemas({
UserSchema,
});
const client = createClient("mongodb://localhost:27017/monarch-example");
const db = createDatabase(client.db(), schemas);
const newUser = await db.collections.users
.insertOne({
name: "anon",
email: "anon@gmail.com",
age: 0,
isVerified: true,
});
const users = await db.collections.users.find({});Design Decisions
Schema as the Single Source of Truth
Instead of writing a schema AND a TypeScript interface, Monarch infers the type from the schema definition:
// The schema IS the type
type User = InferSchemaOutput<typeof UserSchema>;
// {
// name: string;
// email: string;
// age?: number;
// isVerified: boolean;
// }Zero Runtime Overhead
Monarch's type system is entirely compile-time. At runtime, it's a thin wrapper around the native MongoDB driver — no query translation layer, no ORM magic. This means:
- No performance penalty compared to raw MongoDB queries
- No hidden queries or N+1 problems
- Full access to MongoDB's native features when you need them
Built-in Validation
Every field type has built-in validation that runs before writes:
const PostSchema = createSchema("posts", {
title: string().min(1).max(200),
slug: string().regex(/^[a-z0-9-]+$/),
views: number().min(0),
status: literal("draft", "published"),
});
// This throws a validation error at runtime
await db.posts.insertOne({
title: "", // ✗ min length is 1
slug: "Invalid Slug!", // ✗ doesn't match regex
views: -5, // ✗ min is 0
status: "archived", // ✗ not in enum
});Lessons Learned
Building an open-source developer tool taught me a few things:
-
API design is everything. I rewrote the schema builder API three times before it felt right. The final version uses a chainable builder pattern that maps naturally to how developers think about data.
-
TypeScript's type system is incredibly powerful. Monarch's type inference relies on conditional types, mapped types, and recursive generics. It's essentially a compiler within a compiler.
-
Documentation is a feature. The best API in the world is useless if developers can't figure out how to use it. I invested heavily in docs, examples, and error messages.
What's Next
Monarch is actively maintained and growing. Some things on the roadmap:
- Migration support - versioned schema changes with rollback
- Plugin system - extend Monarch with custom field types and hooks
Check it out at monarchorm.com and let me know what you think.