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

typescript
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:

typescript
// 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:

typescript
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:

  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

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

© 2026 Prince