Skip to content

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);  // Date

Insert 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);  // 3

Read

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 false

Delete 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' }
  }
}

Released under the MIT License.