@ioni/nest-ts-valid-mongodb
v0.2.0
Published
High-performance MongoDB Native Driver wrapper for NestJS with Zod schema validation. Lightweight, type-safe, and robust
Maintainers
Readme
@ioni/nest-ts-valid-mongodb
Architect's Choice: High-performance MongoDB Native Driver wrapper for NestJS with Zod schema validation. Lightweight, type-safe, and robust.
🧠 Motivation
Let's be real: Mongoose is heavy. It adds a massive layer of abstraction, hydration, and state management that you often don't need for high-performance microservices.
On the other hand, using the Native MongoDB Driver directly is like running with scissors—fast, but one typo in your query or schema and you're in trouble.
@ioni/nest-ts-valid-mongodb bridges this gap. It provides:
- Native Speed: Zero-overhead wrapper around the official MongoDB driver.
- Zod Safety: Runtime validation using Zod schemas. If it doesn't match the schema, it doesn't get saved.
- NestJS Ergonomics: Proper Dependency Injection, Modules, and Decorators.
✨ Features
- 🚀 Lightweight: ~8KB bundle size. No bloat.
- 🛡️ Zod Powered: Define schemas with Zod, get TypeScript inference for free.
- 💉 Dependency Injection:
forRoot,forRootAsync(ConfigService), andforFeature. - 🔌 Robust: Auto-reconnect patterns and clean shutdown handling.
- 🎨 Decorators:
@InjectModel()and@InjectConnection()for clean code.
📦 Installation
pnpm add @ioni/nest-ts-valid-mongodb mongodb zod @nestjs/common @nestjs/core
# or
npm install @ioni/nest-ts-valid-mongodb mongodb zod @nestjs/common @nestjs/coreNote for Local Development: If you are using a local MongoDB instance (especially with Docker or ReplSets), you might need to append
?directConnection=trueto your connection string to avoid connection timeouts.
Requirements
- Node.js: >= 16.0.0
- NestJS: ^10.0.0 || ^11.0.0
- MongoDB: ^5.0.0 || ^6.0.0
- TypeScript: >= 5.0.0
- Zod: ^3.0.0
🏗️ Complete Example
Here's a full working example showing all the pieces together:
// user.schema.ts
import { z } from 'zod';
export const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18),
role: z.enum(['user', 'admin']).default('user'),
});
export type User = z.infer<typeof UserSchema>;
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TsValidMongoModule } from '@ioni/nest-ts-valid-mongodb';
import { UsersModule } from './users/users.module';
@Module({
imports: [
ConfigModule.forRoot(),
TsValidMongoModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
uri: config.get<string>('MONGO_URI')!,
databaseName: config.get<string>('MONGO_DB_NAME')!,
}),
inject: [ConfigService],
}),
UsersModule,
],
})
export class AppModule {}
// users/users.module.ts
import { Module } from '@nestjs/common';
import { TsValidMongoModule } from '@ioni/nest-ts-valid-mongodb';
import { UserSchema } from '../user.schema';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [
TsValidMongoModule.forFeature([
{
name: 'users',
schema: UserSchema,
indexes: [{ key: { email: 1 }, unique: true }],
},
]),
],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel, Model, Doc } from '@ioni/nest-ts-valid-mongodb';
import { User } from '../user.schema';
@Injectable()
export class UsersService {
constructor(@InjectModel('users') private readonly usersModel: Model<User>) {}
async create(user: User): Promise<Doc<User>> {
return this.usersModel.insert(user);
}
async findAll(): Promise<Doc<User>[]> {
return this.usersModel.find();
}
async findByEmail(email: string): Promise<Doc<User> | null> {
return this.usersModel.findOneBy({ email });
}
async updateRole(id: string, role: 'user' | 'admin'): Promise<Doc<User> | null> {
return this.usersModel.updateById(id, { values: { role } });
}
async delete(id: string): Promise<Doc<User> | null> {
return this.usersModel.deleteById(id);
}
}
// users/users.controller.ts
import { Controller, Get, Post, Body, Param, Delete, Put } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from '../user.schema';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() user: User) {
return this.usersService.create(user);
}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':email')
findOne(@Param('email') email: string) {
return this.usersService.findByEmail(email);
}
@Put(':id/role')
updateRole(@Param('id') id: string, @Body('role') role: 'user' | 'admin') {
return this.usersService.updateRole(id, role);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.delete(id);
}
}🚀 Quick Start
1. Define your Schema
Create a Zod schema. This acts as both your runtime validator and your TypeScript type source.
// cats.schema.ts
import { z } from 'zod';
export const CatSchema = z.object({
name: z.string(),
age: z.number().min(0),
breed: z.string().optional(),
});
export type Cat = z.infer<typeof CatSchema>;2. Import the Module
Register the module in your AppModule.
// app.module.ts
import { Module } from '@nestjs/common';
import { TsValidMongoModule } from '@ioni/nest-ts-valid-mongodb';
import { CatSchema } from './cats.schema';
@Module({
imports: [
// Synchronous configuration
TsValidMongoModule.forRoot({
uri: 'mongodb://localhost:27017',
databaseName: 'test_db',
}),
// Register specific collections (features)
TsValidMongoModule.forFeature([{ name: 'cats', schema: CatSchema }]),
],
})
export class AppModule {}3. Inject and Use
Use the @InjectModel decorator to get your fully typed model.
// cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel, Model } from '@ioni/nest-ts-valid-mongodb';
import { Cat, CatSchema } from './cats.schema';
@Injectable()
export class CatsService {
constructor(@InjectModel('cats') private readonly catModel: Model<Cat>) {}
async create(cat: Cat) {
// This will throw if 'cat' doesn't match the Zod schema!
return await this.catModel.insert(cat);
}
async findAll() {
return await this.catModel.find({});
}
async findByName(name: string) {
return await this.catModel.findOneBy({ name });
}
async updateAge(id: string, age: number) {
return await this.catModel.updateById(id, {
values: { age },
});
}
async delete(id: string) {
return await this.catModel.deleteById(id);
}
}📘 API Reference
Model Methods
The injected Model<T> provides the following type-safe methods:
Query Methods
find(filters?, options?)- Find multiple documentsfindOneBy(filters?, options?)- Find a single documentfindById(id, options?)- Find by ObjectId or string IDcount(filters?, options?)- Count documents matching filtersadvancedFind(config, outputSchema?, options?)- Advanced queries with cursor manipulation
Insert Methods
insert(document, options?)- Insert a single document (validates with Zod)insertMany(documents, options?)- Insert multiple documents
Update Methods
updateById(id, { values, mode? }, options?)- Update by IDupdateOneBy({ values, mode? }, filters, options?)- Update first matching documentupdateMany({ values, mode? }, filters?, options?)- Update multiple documents
The mode parameter supports:
'basic'(default): Partial updates with type safety'advanced': Full MongoDB update operators ($set,$inc, etc.)
Delete Methods
delete(filters?, options?)- Delete multiple documentsdeleteOneBy(filters, options?)- Delete first matching documentdeleteById(id, options?)- Delete by ID
Type Utilities
import { Model, Doc } from '@ioni/nest-ts-valid-mongodb';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>; // { name: string; email: string }
type UserDoc = Doc<User>; // User & { _id: ObjectId; __v?: number }
type UserModel = Model<User>; // Typed model with all CRUD methods⚙️ Advanced Configuration
Async Configuration (ConfigService)
Usually, you don't hardcode credentials. Use forRootAsync:
TsValidMongoModule.forRootAsync({
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGO_URI'),
databaseName: configService.get<string>('MONGO_DB_NAME'),
}),
inject: [ConfigService],
});Accessing the Native Connection
Sometimes you need raw access to the connection (e.g., for transactions or admin operations).
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@ioni/nest-ts-valid-mongodb';
import type { MongoDbClientWrapper } from '@ioni/nest-ts-valid-mongodb';
@Injectable()
export class DatabaseService {
constructor(@InjectConnection() private readonly dbWrapper: MongoDbClientWrapper) {}
async getStats() {
const db = this.dbWrapper.client.getDb();
return await db.stats();
}
async createIndex() {
const db = this.dbWrapper.client.getDb();
return await db.collection('users').createIndex({ email: 1 }, { unique: true });
}
async runTransaction(operations: () => Promise<void>) {
const session = await this.dbWrapper.client.getDb().client.startSession();
try {
await session.withTransaction(operations);
} finally {
await session.endSession();
}
}
}🛡️ Error Handling
The library provides custom error classes for better error management:
Error Types
import {
TsValidMongoError,
TsValidMongoConnectionError,
TsValidMongoConfigurationError,
} from '@ioni/nest-ts-valid-mongodb';TsValidMongoError- Base error class for all library errorsTsValidMongoConnectionError- Thrown when connection failsTsValidMongoConfigurationError- Thrown when configuration is invalid
Example: Handling Errors
import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectConnection, TsValidMongoConnectionError } from '@ioni/nest-ts-valid-mongodb';
@Injectable()
export class HealthService implements OnModuleInit {
constructor(@InjectConnection() private dbWrapper: any) {}
async onModuleInit() {
try {
const db = this.dbWrapper.client.getDb();
await db.command({ ping: 1 });
console.log('✅ Database connection healthy');
} catch (error) {
if (error instanceof TsValidMongoConnectionError) {
console.error('❌ Database connection failed:', error.message);
console.error('Cause:', error.cause);
}
throw error;
}
}
}🔄 Graceful Shutdown
Production applications must handle shutdown gracefully to prevent data loss and connection leaks. This is especially critical in containerized environments (Docker, Kubernetes) where pods are frequently terminated.
Why Graceful Shutdown Matters
When your NestJS application receives a termination signal (SIGTERM, SIGINT), MongoDB connections must be closed properly to:
- Prevent connection leaks that exhaust the connection pool
- Avoid orphaned connections in testing environments
- Ensure clean pod termination in Kubernetes (respecting
terminationGracePeriodSeconds) - Allow in-flight operations to complete before shutdown
Enabling Graceful Shutdown
Enable shutdown hooks in your main.ts:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable graceful shutdown hooks
// This ensures MongoDB connections are properly closed on SIGTERM/SIGINT
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();Configuration Options
You can customize shutdown behavior when configuring the module (Note: These settings are applied at application startup and cannot be changed dynamically):
TsValidMongoModule.forRoot({
uri: 'mongodb://localhost:27017',
databaseName: 'my_database',
// Optional: Graceful shutdown configuration
shutdownTimeout: 10000, // Max time for shutdown process (default: 10000ms)
forceShutdown: false, // Wait for operations vs force close (default: false)
})
// Example: Faster shutdown with force close (use with caution)
TsValidMongoModule.forRoot({
uri: 'mongodb://localhost:27017',
databaseName: 'my_database',
shutdownTimeout: 5000, // Shorter timeout for dev/test environments
forceShutdown: true, // Force immediate close (may interrupt operations)
})Configuration Parameters
shutdownTimeout(number, optional)- Maximum time to wait for the shutdown process to complete
- Applies to each connection close operation independently
- Default:
10000ms (10 seconds) - Must be a positive finite number
- Set to
0for no timeout (not recommended in production)
forceShutdown(boolean, optional)- Whether to force-close MongoDB connections (drops pending operations immediately)
- Default:
false(graceful close - waits for in-flight operations to complete) - When
true: Immediately closes connections, may interrupt active operations - When
false: Waits for pending operations before closing - Use
trueonly when you need immediate termination regardless of data integrity
Kubernetes Integration
For Kubernetes deployments, ensure your terminationGracePeriodSeconds exceeds your shutdown timeout:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nestjs-app
spec:
template:
spec:
terminationGracePeriodSeconds: 30 # Should be > shutdownTimeout
containers:
- name: app
image: my-app:latest
# ... other configStructured Logging
The shutdown process emits structured JSON logs compatible with observability platforms (Datadog, Splunk, etc.).
Pro Tip: By default, NestJS wraps logs in its own text format. To get pure JSON output suitable for log aggregators, we recommend using a JSON-logger compatible with NestJS, such as
nestjs-pino.
// Shutdown initiated
{
"event": "shutdown.start",
"connectionCount": 2,
"timestamp": "2025-01-15T10:30:00.000Z"
}
// Connection closed successfully
{
"event": "connection.closed",
"token": "default",
"durationMs": 45,
"timestamp": "2025-01-15T10:30:00.050Z"
}
// Shutdown completed
{
"event": "shutdown.complete",
"totalConnections": 2,
"successCount": 2,
"failureCount": 0,
"durationMs": 95,
"timestamp": "2025-01-15T10:30:00.100Z"
}Shutdown Behavior
The shutdown process includes built-in resilience features:
- Automatic retries: Failed connection closes are retried up to 2 times with exponential backoff
- Parallel execution: All connections close concurrently for faster shutdown
- Timeout enforcement: Each connection has an independent timeout to prevent hanging
- Graceful degradation: Individual connection failures don't block the shutdown process
Best Practices
- Always enable shutdown hooks (
app.enableShutdownHooks()) in production environments - Use graceful close by default: Keep
forceShutdown: falseunless you have specific requirements - Set appropriate timeout based on your application's workload (typically 5-30 seconds)
- Monitor shutdown logs to detect slow or failing connection closures using structured JSON output
- Test shutdown behavior in your integration tests to ensure connections close cleanly
- Avoid force shutdown unless absolutely necessary - it may cause data inconsistencies
- Coordinate with Kubernetes
terminationGracePeriodSeconds(k8s timeout should be higher thanshutdownTimeout)
🤝 Contributing
We hate bad code. If you want to contribute, ensure:
- Strict Types: No
any(unless you have a damn good reason). - Tests: Unit tests are mandatory.
- Linting: Respect the eslint rules.
🐛 Feedback & Support
This is an actively maintained project. We welcome feedback, bug reports, and feature requests:
📈 Roadmap
- [ ] Comprehensive test suite with 80%+ coverage
- [ ] Multi-tenant connection pools support
- [ ] Performance benchmarks vs Mongoose
- [ ] Support for MongoDB change streams
- [ ] Advanced aggregation pipeline helpers
- [ ] Schema migration utilities
📚 Related Projects
- ts-valid-mongodb - The underlying MongoDB wrapper
- Zod - TypeScript-first schema validation
- NestJS - Progressive Node.js framework
🙏 Acknowledgments
This library is a robust NestJS wrapper for ts-valid-mongodb. We stand on the shoulders of giants—special thanks to the original authors for creating such a lightweight and type-safe foundation for MongoDB interactions.
📄 License
MIT © Ioni
See LICENSE for full details.
