Next.js Integration
Complete guide to using Monch with Next.js App Router.
Project Structure
src/
├── lib/
│ └── models/
│ ├── index.ts
│ ├── user.model.ts
│ └── post.model.ts
├── app/
│ ├── actions/
│ │ └── user.actions.ts
│ ├── users/
│ │ ├── page.tsx
│ │ └── [id]/
│ │ └── page.tsx
│ └── api/
│ └── users/
│ └── route.ts
└── components/
└── UserCard.tsxModel Definition
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),
email: field.email(),
avatar: field.url().optional(),
role: field.enum(['user', 'admin']).default('user'),
},
timestamps: true,
indexes: [
{ key: { email: 1 }, unique: true },
],
});
export type User = ModelOf<typeof Users>;
export type SerializedUser = SerializedOf<typeof Users>;typescript
// lib/models/index.ts
export { Users, type User, type SerializedUser } from './user.model';
export { Posts, type Post, type SerializedPost } from './post.model';Server Components
typescript
// app/users/page.tsx
import { Users, type SerializedUser } from '@/lib/models';
import { UserCard } from '@/components/UserCard';
export default async function UsersPage() {
const users = await Users.find({ role: 'user' })
.sort({ createdAt: -1 })
.limit(20)
.serialize();
return (
<div className="grid gap-4">
{users.map((user) => (
<UserCard key={user._id} user={user} />
))}
</div>
);
}typescript
// app/users/[id]/page.tsx
import { Users } from '@/lib/models';
import { notFound } from 'next/navigation';
interface Props {
params: { id: string };
}
export default async function UserPage({ params }: Props) {
const user = await Users.findById(params.id);
if (!user) {
notFound();
}
const serialized = user.serialize();
return (
<div>
<h1>{serialized.name}</h1>
<p>{serialized.email}</p>
<p>Joined: {new Date(serialized.createdAt).toLocaleDateString()}</p>
</div>
);
}Client Components
typescript
// components/UserCard.tsx
'use client';
import type { SerializedUser } from '@/lib/models';
interface Props {
user: SerializedUser;
}
export function UserCard({ user }: Props) {
return (
<div className="p-4 border rounded">
<h2>{user.name}</h2>
<p>{user.email}</p>
{user.avatar && <img src={user.avatar} alt={user.name} />}
<small>
Joined {new Date(user.createdAt).toLocaleDateString()}
</small>
</div>
);
}Server Actions
typescript
// app/actions/user.actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { Users, type SerializedUser } from '@/lib/models';
import { MonchValidationError } from '@codician-team/monch';
export async function getUsers(): Promise<SerializedUser[]> {
return Users.find()
.sort({ createdAt: -1 })
.serialize();
}
export async function getUser(id: string): Promise<SerializedUser | null> {
const user = await Users.findById(id);
return user?.serialize() ?? null;
}
export async function createUser(formData: FormData) {
try {
const user = await Users.insertOne({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
revalidatePath('/users');
return { success: true, user: user.serialize() };
} catch (error) {
if (error instanceof MonchValidationError) {
return { success: false, errors: error.toFormErrors() };
}
throw error;
}
}
export async function updateUser(id: string, formData: FormData) {
try {
const user = await Users.updateOne(
{ _id: id },
{
$set: {
name: formData.get('name') as string,
email: formData.get('email') as string,
},
}
);
revalidatePath('/users');
revalidatePath(`/users/${id}`);
return { success: true, user: user?.serialize() ?? null };
} catch (error) {
if (error instanceof MonchValidationError) {
return { success: false, errors: error.toFormErrors() };
}
throw error;
}
}
export async function deleteUser(id: string) {
await Users.deleteOne({ _id: id });
revalidatePath('/users');
return { success: true };
}Form with Validation
typescript
// components/UserForm.tsx
'use client';
import { useState } from 'react';
import { createUser } from '@/app/actions/user.actions';
export function UserForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [pending, setPending] = useState(false);
async function handleSubmit(formData: FormData) {
setPending(true);
setErrors({});
const result = await createUser(formData);
if (!result.success) {
setErrors(result.errors);
}
setPending(false);
}
return (
<form action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
required
className={errors.name ? 'border-red-500' : ''}
/>
{errors.name && (
<p className="text-red-500 text-sm">{errors.name}</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
required
className={errors.email ? 'border-red-500' : ''}
/>
{errors.email && (
<p className="text-red-500 text-sm">{errors.email}</p>
)}
</div>
{errors._root && (
<p className="text-red-500">{errors._root}</p>
)}
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create User'}
</button>
</form>
);
}API Routes
typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Users } from '@/lib/models';
import { MonchValidationError } from '@codician-team/monch';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get('page')) || 1;
const limit = Number(searchParams.get('limit')) || 20;
const result = await Users.find()
.sort({ createdAt: -1 })
.paginate({ page, limit });
return NextResponse.json({
users: result.data.map(u => u.serialize()),
pagination: result.pagination,
});
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const user = await Users.insertOne(body);
return NextResponse.json(user.serialize(), { status: 201 });
} catch (error) {
if (error instanceof MonchValidationError) {
return NextResponse.json(
{ errors: error.toFormErrors() },
{ status: 400 }
);
}
throw error;
}
}Pagination Component
typescript
// components/Pagination.tsx
'use client';
import Link from 'next/link';
interface Props {
page: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
baseUrl: string;
}
export function Pagination({ page, totalPages, hasNext, hasPrev, baseUrl }: Props) {
return (
<nav className="flex items-center gap-4">
{hasPrev ? (
<Link href={`${baseUrl}?page=${page - 1}`}>Previous</Link>
) : (
<span className="text-gray-400">Previous</span>
)}
<span>Page {page} of {totalPages}</span>
{hasNext ? (
<Link href={`${baseUrl}?page=${page + 1}`}>Next</Link>
) : (
<span className="text-gray-400">Next</span>
)}
</nav>
);
}Environment Setup
bash
# .env.local
MONGODB_URI=mongodb://localhost:27017/myappDebug Mode in Development
typescript
// lib/db.ts
import { setDebug } from '@codician-team/monch';
if (process.env.NODE_ENV === 'development') {
setDebug(true);
}