NestJS Mongoose Schemas
A Mongoose schema defines the structure of documents stored in a MongoDB collection. It specifies what fields a document has, what types they must be, which fields are required, and what default values to apply. NestJS uses a decorator-based approach to define schemas directly on TypeScript classes — keeping the schema definition clean and type-safe.
Creating a Schema
Use @Schema() on the class and @Prop() on each field:
// user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
@Schema({ timestamps: true })
export class User {
@Prop({ required: true })
name: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true, select: false })
password: string;
@Prop({ default: 'user' })
role: string;
@Prop({ default: true })
isActive: boolean;
}
export const UserSchema = SchemaFactory.createForClass(User);
Prop Options
Option | Meaning ----------------------|------------------------------------------ required: true | Field must be present on every document unique: true | No two documents can share this value default: value | Value used when field is not provided select: false | Excluded from query results by default enum: ['a','b'] | Value must be one of the listed options min / max | Numeric range validation minlength / maxlength | String length validation
Using the Model in a Service
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './user.schema';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name)
private readonly userModel: Model<UserDocument>,
) {}
async findAll(): Promise<User[]> {
return this.userModel.find().exec();
}
async findOne(id: string): Promise<User> {
return this.userModel.findById(id).exec();
}
async create(dto: CreateUserDto): Promise<User> {
const created = new this.userModel(dto);
return created.save();
}
async update(id: string, dto: UpdateUserDto): Promise<User> {
return this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec();
}
async remove(id: string): Promise<User> {
return this.userModel.findByIdAndDelete(id).exec();
}
}
Nested Documents
MongoDB's strength is storing nested data inside a single document. Mongoose schemas support nested objects through embedded schemas:
@Schema()
export class Address {
@Prop() street: string;
@Prop() city: string;
@Prop() country: string;
}
@Schema()
export class User {
@Prop() name: string;
@Prop({ type: Address })
address: Address;
@Prop([String])
tags: string[]; // array of strings
@Prop([{ type: Address }])
shippingAddresses: Address[]; // array of nested objects
}
Schema Flow Diagram
user.schema.ts
@Schema() class User { @Prop() name ... }
|
v
SchemaFactory.createForClass(User)
→ UserSchema (Mongoose schema object)
|
v
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])
|
v
@InjectModel(User.name) userModel: Model<UserDocument>
|
v
userModel.find() / .save() / .findByIdAndUpdate()
|
v
MongoDB Collection: users
Virtual Fields
Virtuals are computed properties that exist on the model but are not stored in MongoDB. Define them after the schema is created:
@Schema()
export class User {
@Prop() firstName: string;
@Prop() lastName: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.virtual('fullName').get(function () {
return `${this.firstName} ${this.lastName}`;
});
Schema Middleware (Hooks)
Mongoose schemas support pre and post hooks that run before or after specific operations. Hashing a password before saving is a common use case:
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.pre('save', async function (next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
The pre('save') hook fires before every .save() call. The this.isModified('password') check prevents re-hashing an already-hashed password on unrelated updates.
References Between Collections
When a document in one collection references a document in another, use mongoose.Schema.Types.ObjectId and the ref option:
import { Types } from 'mongoose';
@Schema()
export class Order {
@Prop({ type: Types.ObjectId, ref: 'User', required: true })
user: Types.ObjectId;
@Prop() total: number;
}
// Populating the reference in a query
const orders = await this.orderModel
.find()
.populate('user') // replaces the ObjectId with the actual User document
.exec();
Mongoose schemas give you the structure and validation that raw MongoDB lacks, while keeping MongoDB's document flexibility. Combined with NestJS's module system and dependency injection, Mongoose integrates seamlessly into your application's service layer.
