Return to notes
2025.10.20

Building Monarch: A Type-Safe ODM for MongoDB

typescriptmongodbopen-sourceorm
DATE
EST. READ
3 min read

The Problem

MongoDB is incredibly flexible, but that flexibility comes at a cost. Without guardrails, your data layer becomes a minefield:

  • Typos in field names go unnoticed until production
  • Schema changes break queries silently
  • There's no autocomplete for your document shape

Mongoose helps, but its TypeScript support has always felt like an afterthought. I wanted something TypeScript-first — where the schema is the source of truth for types, queries, and validation.

That's why I built Monarch.

What Is Monarch?

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:

typescript
import { createDatabase, createSchema, field } from "monarchorm";
 
const UserSchema = createSchema("users", {
  name: field.string().required(),
  email: field.string().required(),
  age: field.number().optional(),
  role: field.enum(["admin", "user"]).default("user"),
  createdAt: field.date().default(() => new Date()),
});
 
const db = createDatabase(client, {
  users: UserSchema,
});
 
// Full autocomplete and type checking
const user = await db.users.findOne({
  email: "prince@example.com",
});
// user.name  ✓  (string)
// user.age   ✓  (number | undefined)
// user.foo   ✗  TypeScript error!

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. One source of truth, zero drift:

typescript
// The schema IS the type
type User = InferSchemaType<typeof UserSchema>;
// {
//   name: string;
//   email: string;
//   age?: number;
//   role: "admin" | "user";
//   createdAt: Date;
// }

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:

typescript
const PostSchema = createSchema("posts", {
  title: field.string().min(1).max(200),
  slug: field.string().regex(/^[a-z0-9-]+$/),
  views: field.number().min(0),
  status: field.enum(["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:

  1. 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.

  2. 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.

  3. 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
  • Aggregation pipeline builder — type-safe aggregation queries

Check it out at monarchorm.com and let me know what you think.

© 2026 Prince