Exploring NestJS with MongoDB: One-to-One and One-to-Many Relationships, Plus AWS S3 File Upload
In my recent project, I had the opportunity to dive deep into using NestJS with MongoDB. One of the core aspects I focused on was handling one-to-one and one-to-many relationships in MongoDB, which is crucial for building scalable and efficient data models. Additionally, I integrated AWS S3 for file uploads, making the application more versatile. Here’s a rundown of my experience.
Understanding Relationships in MongoDB
Unlike SQL databases, MongoDB is a NoSQL database, which means it doesn’t natively support relationships between different collections (equivalent to tables in SQL). However, with the power of Mongoose, an Object Data Modeling (ODM) library for MongoDB and Node.js, we can create and manage relationships quite effectively.
One-to-One Relationship
A one-to-one relationship is where a single document in one collection is related to a single document in another collection. For instance, consider a scenario where each user has a unique set of user settings.
Here’s how I implemented this:
- User Schema:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { UserSettings } from './userSettings.schema';
@Schema()
export class User extends Document {
@Prop({ unique: true, required: true })
username: string;
@Prop({ required: true })
password: string;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'UserSettings' })
settings: UserSettings;
}
export const UserSchema = SchemaFactory.createForClass(User);
User Settings Schema:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
@Schema()
export class UserSettings extends Document {
@Prop()
theme: string;
@Prop()
notificationsEnabled: boolean;
}
export const UserSettingsSchema = SchemaFactory.createForClass(UserSettings);
In this setup, the User
schema references the UserSettings
schema using the ObjectId
. This creates a one-to-one relationship between a user and their settings.
One-to-Many Relationship
A one-to-many relationship is where a single document in one collection is related to multiple documents in another collection. For example, a user might create multiple posts.
Here’s how I modeled this:
- Post Schema:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { User } from './user.schema';
@Schema()
export class Post extends Document {
@Prop({ required: true })
title: string;
@Prop({ required: true })
content: string;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
author: User;
}
export const PostSchema = SchemaFactory.createForClass(Post);
User Schema (with posts):
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { UserSettings } from './userSettings.schema';
import { Post } from './post.schema';
@Schema()
export class User extends Document {
@Prop({ unique: true, required: true })
username: string;
@Prop({ required: true })
password: string;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'UserSettings' })
settings: UserSettings;
@Prop({ type: [mongoose.Schema.Types.ObjectId], ref: 'Post' })
posts: Post[];
}
export const UserSchema = SchemaFactory.createForClass(User);
In this setup, the User
schema has a posts
field that references an array of Post
objects, establishing a one-to-many relationship.
Integrating AWS S3 for File Uploads
Another crucial feature in the project was the ability to upload files to AWS S3. This is essential for applications that need to handle user uploads, like profile pictures or documents.
Here’s how I set up AWS S3 file uploads in my NestJS application:
- Install Necessary Packages:
npm install aws-sdk multer multer-s3
AWS S3 Configuration in the Service: I created a service that handles the file upload logic.
import { Injectable } from '@nestjs/common';
import * as AWS from 'aws-sdk';
import { ConfigService } from '@nestjs/config';
import { Multer } from 'multer';
@Injectable()
export class UploadService {
private s3: AWS.S3;
constructor(private configService: ConfigService) {
this.s3 = new AWS.S3({
accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get<string>('AWS_SECRET_ACCESS_KEY'),
region: this.configService.get<string>('AWS_REGION'),
});
}
async uploadImage(file: Multer.File): Promise<string> {
const uploadResult = await this.s3
.upload({
Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
Key: `${Date.now().toString()}-${file.originalname}`,
Body: file.buffer,
ContentType: file.mimetype,
})
.promise();
return uploadResult.Location; // Return the URL of the uploaded file
}
}
Controller for File Upload: I created a controller to handle file uploads through an API endpoint.
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { UploadService } from './upload.service';
@Controller('upload')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
@Post('image')
@UseInterceptors(FileInterceptor('file'))
async uploadImage(@UploadedFile() file: Express.Multer.File) {
const imageUrl = await this.uploadService.uploadImage(file);
return { imageUrl };
}
}
Environment Variables: I stored the AWS credentials in a .env
file to keep them secure.
Conclusion
Integrating NestJS with MongoDB for one-to-one and one-to-many relationships, along with AWS S3 for file uploads, provided a robust foundation for building scalable applications. NestJS’s powerful architecture, combined with MongoDB’s flexibility, made managing relationships between different data models straightforward. Adding AWS S3 into the mix allowed the application to handle user-generated content efficiently.
This experience has reinforced my belief in choosing the right tools for the job. Whether you’re handling complex data relationships or integrating external services like AWS, NestJS offers a solid framework to build upon. If you’re working on a project that needs these features, I highly recommend giving this stack a try!