Getting started
This tutorial walks you from an empty project to your first working model with Mongoat: connect to MongoDB, define a schema-validated model, and run the core CRUD operations. By the end you will have a User model backed by a real MongoDB collection, with server-side validation and a unique index in place.
Prerequisites
- Node.js
^20.19.0or>=22.12.0 - A MongoDB instance reachable from your machine (local or remote)
1. Install
npm install @iamcalegari/mongoat2. Connect
Create a Database instance and connect to it. The connection URL and credentials can come from config or from environment variables (MONGODB_URI, MONGODB_USERNAME, MONGODB_PASSWORD); the database name comes from MONGODB_DB_NAME or config.dbName:
import { Database } from '@iamcalegari/mongoat';
export const database = new Database({
dbName: 'mongoat-example',
});
await database.connect();If neither MONGODB_URI nor config.uri is set, Mongoat falls back to mongodb://127.0.0.1:27017/ — handy for local development, but make sure a real connection string is configured before you touch a shared environment.
DatabaseConfig extends the driver's MongoClientOptions, so any client option belongs in this same object and is forwarded when connect() runs. Mongoat only fills gaps: it supplies defaults (ignoreUndefined: true is the only one) and your config wins on every key.
Mongoat sets no serverApi of its own. Declaring MongoDB's Stable API is the application's decision, and it is made explicitly:
import { Database, ServerApiVersion } from '@iamcalegari/mongoat';
export const database = new Database({
dbName: 'mongoat-example',
serverApi: { version: ServerApiVersion.v1, strict: true },
});Reach for strict: true only when you know the application uses nothing outside Stable API v1. The server rejects everything else, including $vectorSearch, createSearchIndex and listSearchIndexes:
MongoServerError: $vectorSearch is not allowed with 'apiStrict: true' in API Version 1
code: 323, codeName: APIStrictError3. Define a model
A Model is a typed wrapper around a MongoDB collection. It takes the document shape, a schema describing that shape as a $jsonSchema (used for server-side validation when validity: true), and an optional list of indexes. The schema comes in two equivalent forms — a class annotated with @Schema/@Prop, or a plain ModelValidationSchema object — and both compile to the same server-side validator, so pick whichever tab reads better to you:
import {
CreateIndexProps,
METHODS,
Model,
Optional,
Pre,
Prop,
Schema,
} from '@iamcalegari/mongoat';
@Schema('users')
class UserSchema {
@Prop({ bsonType: 'string', description: 'Username of the user' })
username!: string;
// A field-level pre-hook transforms this field's value before every
// insert — see the "Register pre/post hooks" how-to.
@Pre(METHODS.INSERT, () => 'hashedPassword')
@Prop({ bsonType: 'string', description: 'Password of the user' })
password!: string;
@Prop({
bsonType: 'string',
description: 'Mail of the user',
pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$',
})
mail!: string;
@Prop({ bsonType: 'string', description: 'First name of the user' })
firstName!: string;
@Prop({ bsonType: 'string', description: 'Last name of the user' })
lastName!: string;
@Optional()
@Prop({ bsonType: 'date', description: 'Date of the user creation' })
insertedAt?: Date;
@Optional()
@Prop({ bsonType: 'date', description: 'Date of last update of the user' })
updatedAt?: Date;
}
const indexes: CreateIndexProps[] = [
{
key: { username: 1, mail: 1 },
name: 'unique_username_mail',
unique: true,
},
];
export const User = new Model<UserSchema>({
schema: UserSchema,
indexes,
validity: true,
});
// A fresh timestamp per insert. A field-level @Pre only transforms a field
// that is present on the document — it never materializes an absent one —
// so a value generated on insert lives in a model-level hook instead. The
// class declares `insertedAt` as a field, so no cast is needed — see
// "Document defaults & timestamps".
User.pre(METHODS.INSERT, (ctx) => {
ctx.document.insertedAt = new Date();
});import {
CreateIndexProps,
METHODS,
Model,
ModelValidationSchema,
SchemaWithDefaults,
} from '@iamcalegari/mongoat';
interface UserSchema {
username: string;
password: string;
mail: string;
firstName: string;
lastName: string;
}
const schema: ModelValidationSchema<SchemaWithDefaults<UserSchema>> = {
bsonType: 'object',
properties: {
username: { bsonType: 'string', description: 'Username of the user' },
password: { bsonType: 'string', description: 'Password of the user' },
mail: {
bsonType: 'string',
description: 'Mail of the user',
pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$',
},
firstName: { bsonType: 'string', description: 'First name of the user' },
lastName: { bsonType: 'string', description: 'Last name of the user' },
insertedAt: { bsonType: 'date', description: 'Date of the user creation' },
updatedAt: {
bsonType: 'date',
description: 'Date of last update of the user',
},
},
required: ['firstName', 'lastName', 'mail', 'password', 'username'],
};
const indexes: CreateIndexProps[] = [
{
key: { username: 1, mail: 1 },
name: 'unique_username_mail',
unique: true,
},
];
export const User = new Model<UserSchema>({
collectionName: 'users',
schema,
indexes,
validity: true,
});
// A pre-hook runs before every insert — see the "Register pre/post hooks"
// how-to for the full ctx signature.
User.pre(METHODS.INSERT, (ctx) => {
ctx.document.password = 'hashedPassword';
// A fresh timestamp per insert. `insertedAt` lives in DefaultProperties, not
// UserSchema, so cast to reach it — see "Document defaults & timestamps".
(ctx.document as SchemaWithDefaults<UserSchema>).insertedAt = new Date();
});The two tabs produce the same validator. Note how required is inverted between them: the object form lists required fields explicitly, while in the decorator form every @Prop field is required unless marked @Optional() — which is why the class opts the two timestamp fields out. The decorator form also omits collectionName, because @Schema('users') supplies it as the default. See Define a schema with decorators for the full mapping between the two forms.
new Model(...) must run after the owning Database instance has been constructed — the constructor throws if no Database has been created yet. validity: true restricts the model to the methods needed for typical CRUD (insert, find, findById, findMany, update, updateMany, delete, total); anything not in that list throws at call time.
4. Set up collections and run CRUD
Before using a model, apply its validator and indexes to the database with setupCollections() — this is idempotent, so it's safe to run on every boot:
console.log('⚙️ Setting up collections...');
await database.setupCollections();
const document = await User.insert({
username: 'foobar',
mail: 'foo@bar.com',
password: 'strongPassword',
firstName: 'Foo',
lastName: 'Bar',
});
console.log('DOCUMENT INSERTED: ', document.firstName); // Foo
const updatedDocument = await User.update(
{ _id: document._id },
{
$set: {
firstName: 'John',
lastName: 'Doe',
},
}
);
console.log('DOCUMENT UPDATED: ', updatedDocument?.firstName); // John
await User.insert({
username: 'anotherUser',
mail: 'another@user.com',
password: 'strongPassword',
firstName: 'Another',
lastName: 'User',
});
const documents = await User.findMany();
console.log('ALL DOCUMENTS: ', documents.length); // 2
await User.delete({ username: 'foobar' });
const total = await User.total();
console.log('TOTAL DOCUMENTS: ', total); // 1
await database.disconnect();insert, update, findMany, delete and total all accept the same filter/options shapes as the underlying mongodb driver — Mongoat adds validation, hooks and method gating on top, never a different API surface.
Next steps
- Define a schema with decorators — the decorator form in depth: field decorators, nesting, and hooks on the class.
- Register pre/post hooks — the
ctx-based hook signature in depth, includingfireAndForgetpost-hooks. - Document defaults & timestamps — why the timestamp above uses a hook, and how
documentDefaultsfills in static fields. - Define indexes & validation —
CreateIndexPropsand server-side$jsonSchemavalidation. - Reference — the full public API generated from the source.