Install MongoDB using Docker Compose
MongoDB serves as an open-source NoSQL database management program, particularly adept at handling extensive sets of distributed data. Learning to integrate MongoDB within a Nest.js application involves using the Mongoose package, which is recommended for its robust modeling and validation tools that are absent in the native driver.
Mongoose operates as an object-oriented JavaScript library that facilitates a connection between MongoDB and the Node.js runtime environment. It simplifies the interaction with MongoDB by providing schema validation and the ability to translate between objects in code and their representation within MongoDB, which is a best practice for maintaining data integrity and consistency.
Step 1: Create a new Project
Open your terminal and create a new project with nest-cli
nest new project-name
Choose any name for your project.
Step 2: Install Dependencies
Install these two packages to connect with MongoDB database
npm install @nestjs/mongoose mongoose
Step 3: Setup MongoDB using Docker Compose
Docker Compose is utilized to install MongoDB, streamlining the setup process. Should Docker be unavailable, the MongoDB driver requires manual installation on the machine. It is recommended to use containerization with Docker for database services in development environments, as it offers consistency across different systems and can be integrated seamlessly with NestJS applications.
You have to create a docker-compose.yml file in your root project directory
version: "3"
services:
mongodb:
image: mongo:latest
environment:
- MONGODB_DATABASE="test"
ports:
- 27017:27017
You start the MongoDB server by opening the terminal in your root directory and run:
docker-compose up
You can stop the MongoDB driver using this command:
docker-compose down
Step 4: Connect MongoDB Database using GUI Tool
MongoDB Compass is utilized to interact with the MongoDB database. Installation is necessary for those who do not currently have it. Download MongoDB Compass
Connect with MongoDB
Step 1: Create Mongoose Module in the AppModule
The previous lesson involved the installation of two packages, @nestjs/mongoose and mongoose, for MongoDB integration. The creation of a Mongoose Module within AppModule follows, demonstrating NestJS's module-driven architecture which encapsulates functionality. It is considered a best practice to define schema models and their corresponding modules in separate files to enhance modularity and maintain a clear project structure.
//app.module.ts
@Module({
imports: [MongooseModule.forRoot("mongodb://localhost:27017/spotify-clone")],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
The forRoot() method in NestJS's Mongoose module requires a configuration object analogous to the one used in mongoose.connect() from the Mongoose package. This method provides a streamlined approach to configuring the database connection at the root module level, ensuring all sub-modules can interact with the database consistently. A best practice entails validating the database connection parameters and handling connection errors gracefully to maintain the stability of the application.
Create Schema
In Mongoose, everything commences with a Schema, which maps to a MongoDB collection and dictates the structure of the documents in that collection. Schemas serve to establish Models, with Models being accountable for the creation and retrieval of documents from the MongoDB database. Adopting Schemas and Models aligns with NestJS's modular approach, enhancing maintainability and promoting adherence to an application's defined data architecture. Utilizing Mongoose with NestJS adheres to a best practice of encapsulating database interactions, which improves code reusability and testability.
Step 1: Create a Schema
Create a songs folder and create a schemas folder inside the songs. This structure, recommended in NestJS, promotes a modular architecture by encapsulating schema definitions within their respective domain context, which aligns with domain-driven design principles for maintainable code organization.
//src/songs/schemas/song.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
export type SongDocument = HydratedDocument<Song>;
@Schema()
export class Song {
@Prop({
required: true,
})
title: string;
@Prop({
required: true,
})
releasedDate: Date;
@Prop({
required: true,
})
duration: string;
lyrics: string;
}
export const SongSchema = SchemaFactory.createForClass(Song);
The SongDocument is utilized upon injecting the Model into the SongService. It is a NestJS-specific approach to apply TypeScript interfaces for Mongoose models, promoting type safety and IntelliSense in the service layer.
Applying the @Schema() decorator designates a class as a schema definition, associating the Song class with a MongoDB collection named songs. This decorator is part of NestJS's Mongoose integration, which simplifies working with MongoDB by automatically pluralizing the model name for the collection.
The @Prop() decorator is employed to declare a property within the document. This decorator is crucial in defining the schema's data structure and ensuring the fields align with the intended types in the MongoDB collection.
SchemaFactory is tasked with generating the bare schema definition. Employing console.log on SongSchema will reveal the structured outcome, demonstrating the schema's conversion to a format that Mongoose can use to enforce document structure in MongoDB.
Schema {
obj: {
title: { required: true, type: [Function: String] },
releasedDate: { required: true, type: [Function: Date] },
duration: { required: true, type: [Function: String] }
},
paths: {
title: SchemaString {
enumValues: [],
regExp: null,
path: 'title',
instance: 'String',
validators: [Array],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaStringOptions],
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
releasedDate: SchemaDate {
path: 'releasedDate',
instance: 'Date',
validators: [Array],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaDateOptions],
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
duration: SchemaString {
enumValues: [],
regExp: null,
path: 'duration',
instance: 'String',
validators: [Array],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaStringOptions],
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
_id: ObjectId {
path: '_id',
instance: 'ObjectId',
validators: [],
getters: [],
setters: [Array],
_presplitPath: [Array],
options: [SchemaObjectIdOptions],
_index: null,
defaultValue: [Function],
[Symbol(mongoose#schemaType)]: true
}
},
aliases: {},
subpaths: {},
virtuals: {},
singleNestedPaths: {},
nested: {},
inherits: {},
callQueue: [],
_indexes: [],
methods: {},
methodOptions: {},
statics: {},
tree: {
title: { required: true, type: [Function: String] },
releasedDate: { required: true, type: [Function: Date] },
duration: { required: true, type: [Function: String] },
_id: { auto: true, type: 'ObjectId' }
},
query: {},
childSchemas: [],
plugins: [],
'$id': 1,
mapPaths: [],
s: { hooks: Kareem { pres: Map(0) {}, posts: Map(0) {} } },
_userProvidedOptions: {},
options: {
typeKey: 'type',
id: true,
_id: true,
validateBeforeSave: true,
read: null,
shardKey: null,
discriminatorKey: '__t',
autoIndex: null,
minimize: true,
optimisticConcurrency: false,
versionKey: '__v',
capped: false,
bufferCommands: true,
strictQuery: false,
strict: true
}
}
Save Record in MongoDB
The record must be saved in the MongoDB collection, necessitating the creation of a POST endpoint to store the songs. Utilizing NestJS's @Controller and @Post decorators, the endpoint can be efficiently set up, showcasing the framework's streamlined approach to REST API development. It is considered a best practice to abstract the interaction with MongoDB into a service, which allows for cleaner controllers and easier maintenance of database operations.
Step 1: Create Songs Module
nest g module songs
Step 2: Create Songs Controller
nest g controller songs
Step 3: Create Songs Service
nest g service songs
Step 4: Add a create method in SongsController
import { Body, Controller, Post } from "@nestjs/common";
import { CreateSongDTO } from "./dto/create-song-dto";
import { SongsService } from "./songs.service";
@Controller("songs")
export class SongsController {
constructor(private songService: SongsService) {}
@Post()
create(
@Body()
createSongDTO: CreateSongDTO
) {
return this.songService.create(createSongDTO);
}
}
Step 5: Create CreateSongDTO
// songs/dto/create-song-dto.ts
export class CreateSongDTO {
title: string;
releasedDate: Date;
duration: Date;
lyrics: string;
}
Step 6: Add a create method in SongsService
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Song, SongDocument } from "./schemas/song.schema";
import { Model } from "mongoose";
import { CreateSongDTO } from "./dto/create-song-dto";
@Injectable()
export class SongsService {
constructor(
@InjectModel(Song.name) //1
private readonly songModel: Model<SongDocument> //2
) {}
async create(createSongDTO: CreateSongDTO): Promise<Song> {
const song = await this.songModel.create(createSongDTO); //3.
return song;
}
}
Registration of the song model within the SongsModule allows for its injection into the service, demonstrating NestJS's modular design that promotes loose coupling and high cohesion.
Utilization of the SongDocument type within the song schema file ensures that the object adheres to the defined schema, a practice that enforces type safety and reduces runtime errors.
The songModel incorporates a method to persist records in MongoDB, which illustrates the encapsulation of database operations within models, a practice that enhances maintainability and scalability of the application.
Step 7: Register the Song Model in SongsModule
@Module({
imports: [
MongooseModule.forFeature([{ name: Song.name, schema: SongSchema }]), //1
],
controllers: [SongsController],
providers: [SongsService],
})
export class SongsModule {}
The MongooseModule employs the forFeature() method to configure itself, allowing specific models to be registered within the current scope. It is a NestJS-specific mechanism that ensures model encapsulation and modularity, a recommended approach for maintaining clean and manageable database-related code.
Step 8: Test the Application
The application's functionality can tested. See if it works.
POST http://localhost:3000/songs
Content-Type: application/json
{
"title": "Lasting Lover",
"releasedDate" : "2023-05-11",
"duration" :"02:33",
"lyrics": "I don't know why I can't quite get you out my sight You're always just behind"
}
Find and Delete Record
Step 1: Create a new find method in SongService
A new find method is crafted within the SongService, encapsulating the logic for retrieving song data. As a best practice within NestJS, this method should be designed as a service that can be injected into controllers, promoting a clean separation of concerns and enhanced testability.
//songs.service.ts
async find(): Promise<Song[]> {
return this.songModel.find();
}
Step 2: Create a Route in SongController
A route in the SongController is established to handle specific music-related requests. In NestJS, it's advisable to use decorators like @Get, @Post, or @Put to clearly define the purpose and nature of the route, enhancing the readability and structure of the code.
//songs.controller.ts
@Get()
find(): Promise<Song[]> {
return this.songService.find();
}
Now test this route by starting the application and sending the request at GET http://localhost:3000/songs
Step 3: Create a findById method in SongService
The creation of a findById method within SongService serves to encapsulate the logic for retrieving a specific song by its identifier. Implementing such methods aligns with NestJS's philosophy of modular services, ensuring that each service has a single responsibility and that the application remains scalable and maintainable. It is considered a best practice to abstract database queries within services to isolate them from controllers, thereby promoting clean separation of concerns.
async findById(id: string): Promise<Song> {
return this.songModel.findById(id);
}
Step 4: Create findOne Route in the Controller
The creation of a findOne route in the controller is implemented to facilitate the retrieval of a single song entity by its unique identifier. In NestJS, best practices suggest utilizing decorators like @Get with the route path and @Param to capture route parameters, enhancing the modularity and declarative nature of routing mechanisms.
@Get(':id')
findOne(
@Param('id')
id: string,
):
Promise<Song> {
return this.songService.findById(id);
}
Step 5: Create a delete song method in SongService
A delete song method within SongService can be established to handle removal operations for songs. Ensuring the method is idempotent, meaning it can be called multiple times without changing the result beyond the initial application, is considered a best practice for robust API design in NestJS applications.
async delete(id: string) {
return this.songModel.deleteOne({ _id: id });
}
Step 6: Create a Route for deleting a song
A route for deleting a song is established through a controller's method decorated with NestJS's @Delete() decorator, which maps HTTP DELETE requests to the corresponding service function. In terms of best practices, implementing soft deletion, where records are flagged as inactive rather than removed from the database, can be advantageous for data recovery and audit purposes.
@Delete(':id')
delete(
@Param('id')
id: string,
) {
return this.songService.delete(id);
}
Populate
This lesson covers the addition of relations between two models in Mongoose, illustrating the association where each song is linked to a single album and each album may encompass numerous songs. The demonstration focuses on Mongoose's populate feature, which is pivotal for managing document relationships in MongoDB, akin to JOINs in relational databases. NestJS supports Mongoose's features directly through its dedicated @nestjs/mongoose package, and employing populate is a common practice to efficiently retrieve related documents.
Step 1: Create an album schema
A new folder named albums must be created to house the schema file. This action conforms to NestJS's modular architecture, where separating concerns by feature enhances maintainability and scalability—a best practice in software development.
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument, Schema as MongooseSchema, Types } from "mongoose";
import { Song } from "src/songs/schemas/song.schema";
export type AlbumDocument = HydratedDocument<Album>;
@Schema()
export class Album {
@Prop({
required: true,
})
title: string;
@Prop({ type: [Types.ObjectId], ref: "songs" }) //1
songs: Song[];
}
export const AlbumSchema = SchemaFactory.createForClass(Album);
The songs property is defined in the Album model with the type specified as an array of MongoDB ObjectIDs, intended to store references to the IDs of the song collection within the albums table.
The ref attribute establishes a reference to the songs collection, enabling the creation of relational data structures within a MongoDB database, a feature NestJS leverages through its Mongoose module integration.
As a best practice, it is recommended to clearly document such relationships within the code to improve maintainability and provide clarity for future development efforts. Additionally, ensuring that the ObjectIDs used in references are validated for their format can prevent runtime errors and maintain data integrity.
Step 2: Add a relation in the song schema
Inclusion of a relation within the song schema can be executed through the use of decorators that define the relationship type, such as @OneToMany or @ManyToOne, which NestJS leverages from TypeORM. This practice encapsulates the relational aspect directly within the entity, thus promoting a clear and maintainable structure within the application’s domain model. Best practice dictates careful planning of entity relationships to optimize query performance and database integrity.
// song.schema.ts
@Prop({
type: Types.ObjectId,
ref: Album.name,
})
album: Album;
Step 3: Create Album Module, Controller and Service
The creation of an Album Module, Controller, and Service in a NestJS application encapsulates the album-related functionalities, aligning with the framework's modular architecture. Implementing each as a separate entity follows NestJS's single-responsibility principle, ensuring that the application remains scalable and maintainable. As a best practice, defining strict interfaces and DTOs (Data Transfer Objects) for service methods enhances type safety and validation, which is a crucial aspect of robust software design.
import { Module } from "@nestjs/common";
import { AlbumController } from "./album.controller";
import { AlbumService } from "./album.service";
import { MongooseModule } from "@nestjs/mongoose";
import { Album, AlbumSchema } from "./schemas/album.schema";
@Module({
imports: [
MongooseModule.forFeature([{ name: Album.name, schema: AlbumSchema }]),
],
controllers: [AlbumController],
providers: [AlbumService],
})
export class AlbumModule {}
import { Injectable } from "@nestjs/common";
import { Album, AlbumDocument } from "./schemas/album.schema";
import { Model } from "mongoose";
import { InjectModel } from "@nestjs/mongoose";
import { CreateAlbumDTO } from "./dto/create-album-dto";
import { Song } from "src/songs/schemas/song.schema";
@Injectable()
export class AlbumService {
constructor(
@InjectModel(Album.name)
private readonly albumModel: Model<AlbumDocument>
) {}
async createAlbum(createAlbumDTO: CreateAlbumDTO): Promise<Album> {
return this.albumModel.create(createAlbumDTO);
}
async findAlbums() {
return this.albumModel.find().populate("songs", null, Song.name); //1
}
}
The populate method is applied on the album model to retrieve all songs associated with each album. Utilizing this feature in NestJS effectively allows for eager loading of related entities, which streamlines data retrieval processes. It is considered a good practice to carefully manage such operations to optimize query performance and maintain data integrity.
//create-album-dto.ts
export class CreateAlbumDTO {
title: string;
songs: string[];
}
//album.controller.ts
import { Body, Controller, Get, Post } from "@nestjs/common";
import { Album } from "./schemas/album.schema";
import { AlbumService } from "./album.service";
import { CreateAlbumDTO } from "./dto/create-album-dto";
@Controller("albums")
export class AlbumController {
constructor(private albumService: AlbumService) {}
@Post()
create(
@Body()
createAlbumDTO: CreateAlbumDTO
): Promise<Album> {
return this.albumService.createAlbum(createAlbumDTO);
}
@Get()
find(): Promise<Album[]> {
return this.albumService.findAlbums();
}
}
Refactor the CreateSongDTO
//create-song-dto.ts
album: string;
$7 bundle of my best NestJS + backend developer Courses.
More details coming soon.
Get the latest insights from the marketing world.