Basic CRUD Example
A complete example showing all CRUD operations with Monch.
Setup
typescript
// lib/models/user.model.ts
import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch';
export const Users = collection({
name: 'users',
schema: {
_id: field.id(),
name: field.string().min(1).max(100),
email: field.email(),
role: field.enum(['user', 'admin', 'moderator']).default('user'),
profile: field.object({
bio: field.string().max(500).optional(),
avatar: field.url().optional(),
}).optional(),
settings: field.object({
notifications: field.boolean().default(true),
theme: field.enum(['light', 'dark', 'system']).default('system'),
}),
},
timestamps: true,
indexes: [
{ key: { email: 1 }, unique: true },
{ key: { role: 1 } },
{ key: { createdAt: -1 } },
],
});
export type User = ModelOf<typeof Users>;
export type SerializedUser = SerializedOf<typeof Users>;Create
Insert One
typescript
const user = await Users.insertOne({
name: 'Alice Johnson',
email: 'alice@example.com',
profile: {
bio: 'Software developer',
},
});
console.log(user._id); // ObjectId
console.log(user.role); // 'user' (default)
console.log(user.createdAt); // DateInsert Many
typescript
const users = await Users.insertMany([
{ name: 'Bob Smith', email: 'bob@example.com' },
{ name: 'Carol White', email: 'carol@example.com', role: 'admin' },
{ name: 'Dave Brown', email: 'dave@example.com' },
]);
console.log(users.length); // 3Read
Find One
typescript
const user = await Users.findOne({ email: 'alice@example.com' });
if (user) {
console.log(user.name); // 'Alice Johnson'
}Find by ID
typescript
const user = await Users.findById('507f1f77bcf86cd799439011');
// or
const user = await Users.findById(user._id);Find Many
typescript
// All users
const allUsers = await Users.find().toArray();
// With filter
const admins = await Users.find({ role: 'admin' }).toArray();
// With sorting and limit
const recentUsers = await Users.find()
.sort({ createdAt: -1 })
.limit(10)
.toArray();
// With projection
const emails = await Users.find()
.project({ email: 1, name: 1 })
.toArray();Pagination
typescript
const { data, pagination } = await Users.find({ role: 'user' })
.sort({ createdAt: -1 })
.paginate({ page: 1, limit: 20 });
console.log(`Page ${pagination.page} of ${pagination.totalPages}`);
console.log(`Total users: ${pagination.total}`);Count and Exists
typescript
const userCount = await Users.count({ role: 'user' });
const hasAdmins = await Users.exists({ role: 'admin' });
const totalEstimate = await Users.estimatedDocumentCount();Distinct Values
typescript
const roles = await Users.distinct('role');
// ['user', 'admin', 'moderator']Update
Update One
typescript
const updated = await Users.updateOne(
{ email: 'alice@example.com' },
{ $set: { role: 'admin' } }
);
if (updated) {
console.log(updated.role); // 'admin'
}Update Many
typescript
const count = await Users.updateMany(
{ role: 'user' },
{ $set: { 'settings.notifications': true } }
);
console.log(`Updated ${count} users`);Upsert
typescript
const user = await Users.updateOne(
{ email: 'new@example.com' },
{
$set: { role: 'user' },
$setOnInsert: { name: 'New User' },
},
{ upsert: true }
);Atomic Update
typescript
const user = await Users.findOneAndUpdate(
{ email: 'alice@example.com' },
{ $inc: { loginCount: 1 } },
{ returnDocument: 'after' }
);Delete
Delete One
typescript
const deleted = await Users.deleteOne({ email: 'alice@example.com' });
console.log(deleted); // true or falseDelete Many
typescript
const count = await Users.deleteMany({ role: 'inactive' });
console.log(`Deleted ${count} users`);Atomic Delete
typescript
const deletedUser = await Users.findOneAndDelete({ email: 'alice@example.com' });
if (deletedUser) {
console.log(`Deleted user: ${deletedUser.name}`);
}Express API (Automatic Serialization)
Documents have toJSON() which is called automatically by res.json():
typescript
import express from 'express';
import { Users } from './models';
const app = express();
app.use(express.json());
// List users - toJSON() called automatically
app.get('/api/users', async (req, res) => {
const users = await Users.find({}).toArray();
res.json(users); // Just works!
});
// Get single user
app.get('/api/users/:id', async (req, res) => {
const user = await Users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user); // toJSON() handles BSON types
});
// Create user
app.post('/api/users', async (req, res) => {
const user = await Users.insertOne(req.body);
res.status(201).json(user);
});
// Update user
app.put('/api/users/:id', async (req, res) => {
const user = await Users.updateOne(
{ _id: req.params.id },
{ $set: req.body }
);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
// Delete user
app.delete('/api/users/:id', async (req, res) => {
const deleted = await Users.deleteOne({ _id: req.params.id });
if (!deleted) return res.status(404).json({ error: 'Not found' });
res.json({ success: true });
});Next.js (Manual Serialization)
Next.js blocks toJSON() at the Server/Client boundary. Use .serialize():
typescript
// Server Component
export default async function UsersPage() {
const users = await Users.find({ role: 'user' })
.sort({ createdAt: -1 })
.limit(10)
.serialize(); // Required for Next.js
return <UserList users={users} />;
}typescript
// Client Component
'use client';
import type { SerializedUser } from '@/lib/models/user.model';
interface Props {
users: SerializedUser[];
}
export function UserList({ users }: Props) {
return (
<ul>
{users.map((user) => (
<li key={user._id}>
{user.name} ({user.email})
<small>Joined: {new Date(user.createdAt).toLocaleDateString()}</small>
</li>
))}
</ul>
);
}Error Handling
typescript
import { MonchValidationError } from '@codician-team/monch';
try {
await Users.insertOne({
name: '', // Invalid: too short
email: 'not-an-email', // Invalid: not email format
});
} catch (error) {
if (error instanceof MonchValidationError) {
console.log(error.toFormErrors());
// { name: 'String must contain at least 1 character(s)', email: 'Invalid email' }
}
}