HonoHub Logo

HonoHub

Valibot

Using momos with Valibot validation

Installation

Install momos with Valibot:

npm install momos mongodb valibot

Basic Setup

import { MongoClient } from "mongodb";
import { defineCollection } from "momos";
import * as v from "valibot";

const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("myapp");

Defining Schemas

Simple Schema

const userSchema = v.object({
  name: v.pipe(v.string(), v.minLength(1, "Name is required")),
  email: v.pipe(v.string(), v.email("Invalid email address")),
  age: v.pipe(v.number(), v.integer(), v.minValue(0, "Age must be positive")),
});

const users = defineCollection(db, "users", userSchema);

Schema with Defaults

const postSchema = v.object({
  title: v.string(),
  content: v.string(),
  slug: v.pipe(v.string(), v.regex(/^[a-z0-9-]+$/)),
  views: v.optional(v.number(), 0),
  likes: v.optional(v.number(), 0),
  published: v.optional(v.boolean(), false),
  createdAt: v.optional(v.date(), () => new Date()),
  updatedAt: v.optional(v.date(), () => new Date()),
});

const posts = defineCollection(db, "posts", postSchema);

// Insert with minimal data - defaults are applied
await posts.insertOne({
  title: "Hello World",
  content: "My first post",
  slug: "hello-world",
});

Nested Objects

const addressSchema = v.object({
  street: v.string(),
  city: v.string(),
  state: v.pipe(v.string(), v.length(2)),
  zip: v.pipe(v.string(), v.regex(/^\d{5}(-\d{4})?$/)),
  country: v.optional(v.string(), "USA"),
});

const companySchema = v.object({
  name: v.string(),
  industry: v.string(),
  headquarters: addressSchema,
  offices: v.optional(v.array(addressSchema), []),
});

const companies = defineCollection(db, "companies", companySchema);

Arrays

const productSchema = v.object({
  name: v.string(),
  tags: v.pipe(
    v.array(v.string()),
    v.minLength(1, "At least one tag required")
  ),
  variants: v.array(v.object({
    sku: v.string(),
    color: v.string(),
    size: v.picklist(["S", "M", "L", "XL"]),
    price: v.pipe(v.number(), v.minValue(0)),
  })),
});

const products = defineCollection(db, "products", productSchema);

Enums with Picklist

const orderSchema = v.object({
  orderNumber: v.string(),
  status: v.picklist(["pending", "processing", "shipped", "delivered", "cancelled"]),
  priority: v.optional(
    v.picklist(["low", "normal", "high", "urgent"]),
    "normal"
  ),
});

const orders = defineCollection(db, "orders", orderSchema);

// Type-safe status updates
await orders.updateOne(
  { orderNumber: "ORD-001" },
  { $set: { status: "shipped" } }
);

Optional and Nullable

const profileSchema = v.object({
  username: v.string(),
  bio: v.optional(v.string()),           // Can be undefined
  avatar: v.nullable(v.pipe(v.string(), v.url())), // Can be null
  website: v.nullish(v.pipe(v.string(), v.url())), // Can be null or undefined
});

const profiles = defineCollection(db, "profiles", profileSchema);

Union Types

// Simple union
const idSchema = v.union([v.string(), v.number()]);

// Variant for discriminated unions
const notificationSchema = v.variant("type", [
  v.object({
    type: v.literal("email"),
    to: v.pipe(v.string(), v.email()),
    subject: v.string(),
    body: v.string(),
  }),
  v.object({
    type: v.literal("sms"),
    phone: v.string(),
    message: v.pipe(v.string(), v.maxLength(160)),
  }),
  v.object({
    type: v.literal("push"),
    deviceToken: v.string(),
    title: v.string(),
    body: v.string(),
  }),
]);

const notifications = defineCollection(db, "notifications", notificationSchema);

// Type-safe inserts
await notifications.insertOne({
  type: "email",
  to: "user@example.com",
  subject: "Welcome!",
  body: "Thanks for signing up.",
});

Transformations

const userSchema = v.object({
  email: v.pipe(
    v.string(),
    v.email(),
    v.transform((val) => val.toLowerCase())
  ),
  name: v.pipe(
    v.string(),
    v.transform((val) => val.trim())
  ),
  username: v.pipe(
    v.string(),
    v.minLength(3),
    v.maxLength(20),
    v.transform((val) => val.toLowerCase()),
    v.regex(/^[a-z0-9_]+$/)
  ),
});

const users = defineCollection(db, "users", userSchema);

// Data is transformed before insertion
await users.insertOne({
  email: "JOHN@EXAMPLE.COM", // Stored as "john@example.com"
  name: "  John Doe  ",     // Stored as "John Doe"
  username: "JohnDoe123",   // Stored as "johndoe123"
});

Custom Validation

const passwordSchema = v.pipe(
  v.string(),
  v.minLength(8, "Password must be at least 8 characters"),
  v.regex(/[A-Z]/, "Password must contain uppercase"),
  v.regex(/[a-z]/, "Password must contain lowercase"),
  v.regex(/[0-9]/, "Password must contain a number")
);

const userSchema = v.pipe(
  v.object({
    username: v.string(),
    password: passwordSchema,
    confirmPassword: v.string(),
  }),
  v.forward(
    v.check(
      (input) => input.password === input.confirmPassword,
      "Passwords don't match"
    ),
    ["confirmPassword"]
  )
);

Custom ObjectId Validation

import { ObjectId } from "mongodb";

const objectIdSchema = v.pipe(
  v.custom<ObjectId | string>(
    (val) => val instanceof ObjectId || (typeof val === "string" && ObjectId.isValid(val)),
    "Invalid ObjectId"
  ),
  v.transform((val) => 
    val instanceof ObjectId ? val : new ObjectId(val as string)
  )
);

const commentSchema = v.object({
  postId: objectIdSchema,
  authorId: objectIdSchema,
  content: v.string(),
  createdAt: v.optional(v.date(), () => new Date()),
});

const comments = defineCollection(db, "comments", commentSchema);

Complete Example

import { MongoClient, ObjectId } from "mongodb";
import { defineCollection, ValidationError } from "momos";
import * as v from "valibot";

// Connect to MongoDB
const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("blog");

// Define schemas
const authorSchema = v.object({
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
  bio: v.optional(v.string()),
});

const postSchema = v.object({
  title: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
  slug: v.pipe(v.string(), v.regex(/^[a-z0-9-]+$/)),
  content: v.string(),
  excerpt: v.optional(v.pipe(v.string(), v.maxLength(500))),
  author: authorSchema,
  tags: v.optional(v.array(v.string()), []),
  status: v.optional(
    v.picklist(["draft", "published", "archived"]),
    "draft"
  ),
  views: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)), 0),
  createdAt: v.optional(v.date(), () => new Date()),
  updatedAt: v.optional(v.date(), () => new Date()),
});

// Create typed collection
const posts = defineCollection(db, "posts", postSchema);

// Create indexes
await posts.createIndex({ slug: 1 }, { unique: true });
await posts.createIndex({ "author.email": 1 });
await posts.createIndex({ tags: 1 });
await posts.createIndex({ status: 1, createdAt: -1 });

// Insert a post
try {
  await posts.insertOne({
    title: "Getting Started with momos",
    slug: "getting-started-with-momos",
    content: "momos is a type-safe MongoDB wrapper...",
    author: {
      name: "John Doe",
      email: "john@example.com",
    },
    tags: ["mongodb", "typescript", "tutorial"],
  });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error("Validation failed:", error.issues);
  }
  throw error;
}

// Query posts
const publishedPosts = await posts
  .find({ status: "published" })
  .sort({ createdAt: -1 })
  .limit(10)
  .toArray();

// Update with type safety
await posts.updateOne(
  { slug: "getting-started-with-momos" },
  {
    $set: { status: "published" },
    $inc: { views: 1 },
    $currentDate: { updatedAt: true },
  }
);

// Aggregation
type TagStats = { _id: string; count: number };

const popularTags = await posts
  .aggregate<TagStats>([
    { $match: { status: "published" } },
    { $unwind: "$tags" },
    { $group: { _id: "$tags", count: { $sum: 1 } } },
    { $sort: { count: -1 } },
    { $limit: 10 },
  ])
  .toArray();

console.log("Popular tags:", popularTags);

Error Handling

import { ValidationError } from "momos";

try {
  await users.insertOne({
    name: "",
    email: "not-an-email",
    age: -5,
  });
} catch (error) {
  if (error instanceof ValidationError) {
    error.issues.forEach((issue) => {
      console.log(`${issue.path?.join(".")}: ${issue.message}`);
    });
  }
}

Why Valibot?

Valibot offers several advantages:

  • Tiny bundle size - Modular design means you only bundle what you use
  • Type-safe - Full TypeScript inference
  • Fast - Optimized for performance
  • Standard Schema - Works seamlessly with momos