Welcome to this lesson on integrating MongoDB into your NestJS application! In this lesson, we'll build upon our existing To-Do application an add some valuable enterprise-level features. We will be continuing from where we left off last course. Today, we will set the foundation by understanding databases and how MongoDB fits into our application architecture.
Databases are essential components of web applications because they store and manage the data that applications operate on. There are different types of databases, including relational databases like MySQL and PostgreSQL, and NoSQL databases like MongoDB. MongoDB is a NoSQL database that stands out for its flexibility and scalability, making it an excellent choice for modern web applications.
To get started, you need to set up MongoDB. While our CodeSignal environment comes pre-installed with MongoDB, it's important to know how to set it up on your own machine for a deeper understanding.
-
Install Homebrew (if not already installed):
- Follow the instructions on the Homebrew website to install Homebrew.
-
Install MongoDB with Homebrew:
- Open your terminal and run the following command:
Bash
1brew tap mongodb/brew 2brew install mongodb-community
- Open your terminal and run the following command:
-
Run MongoDB:
- Start the MongoDB server by running the following command:
Bash
1brew services start mongodb/brew/mongodb-community
- This starts the MongoDB server locally, typically accessible on
mongodb://localhost:27017
.
- Start the MongoDB server by running the following command:
But remember, if you are using the CodeSignal environment for this course, MongoDB is pre-installed, and you can directly start coding without worrying about these setup steps.
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It helps in managing relationships between data, provides schema validation, and is used to translate between objects in code and their representation in MongoDB.
To install Mongoose
, use the following command in your terminal within your project directory:
Bash1npm install mongoose @nestjs/mongoose
Next, we'll configure Mongoose
in our NestJS application. Open src/app.module.ts
and set up the connection to MongoDB and import the ToDo
module:
TypeScript1import { Module } from '@nestjs/common'; 2import { MongooseModule } from '@nestjs/mongoose'; 3 4import { TodoModule } from './todo/todo.module'; 5 6@Module({ 7 imports: [ 8 MongooseModule.forRoot('mongodb://localhost/nestjs-todo'), 9 TodoModule, 10 ], 11}) 12export class AppModule {}
Here’s what’s happening:
MongooseModule.forRoot
establishes a connection to a MongoDB instance running locally (mongodb://localhost/nestjs-todo
).- The
TodoModule
will manage our ToDo-related functionality, which we'll define next.
Schemas define the structure of documents in MongoDB. Using Mongoose
, we can create a schema for our ToDo items.
Create a file src/todo/schemas/todo.schema.ts
and define the schema as follows:
TypeScript1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2import { HydratedDocument } from 'mongoose'; 3 4export type TodoDocument = HydratedDocument<Todo>; 5 6@Schema() 7export class Todo { 8 @Prop({ required: true }) 9 title: string; 10 11 @Prop() 12 description: string; 13 14 @Prop({ default: false }) 15 completed: boolean; 16} 17 18export const TodoSchema = SchemaFactory.createForClass(Todo);
Explanation:
@Schema()
: Decorator to mark this class as a Mongoose schema.@Prop()
: Decorator to define schema properties.HydratedDocument<T>
: Mongoose type that represents a document from MongoDB, allows us to use methods likefindById
,findByIdAndUpdate
,delete
, and other Mongoose document methods.SchemaFactory.createForClass(Todo)
is a method provided by NestJS to automatically convert theTodo
class into a Mongoose schema.
The service layer contains the business logic of the application. It will handle creating, reading, updating, and deleting ToDo items. In our previous course, we used an in-memory array for the data. That goes away as soon as you restart your NestJS application. By using a database, we can persist these items beyond the scope of the server process.
Create a file src/todo/todo.service.ts
and implement the service as follows:
TypeScript1import { Injectable } from '@nestjs/common'; 2import { InjectModel } from '@nestjs/mongoose'; 3import { Model } from 'mongoose'; 4import { Todo, TodoDocument } from './schemas/todo.schema'; 5import { CreateTodoDto, UpdateTodoDto } from './dtos/todo.dto'; 6 7@Injectable() 8export class TodoService { 9 constructor(@InjectModel(Todo.name) private readonly todoModel: Model<TodoDocument>) {} 10 11 async findAll(showIncomplete?: boolean): Promise<TodoDocument[]> { 12 const filter: any = {}; 13 14 if (showIncomplete) { 15 filter.completed = false; 16 } 17 18 return await this.todoModel.find(filter).exec(); 19 } 20 21 async findOne(id: string): Promise<TodoDocument | null> { 22 return this.todoModel.findById(id).exec(); 23 } 24 25 async createTodo(todo: CreateTodoDto): Promise<TodoDocument> { 26 const createdTodo = new this.todoModel(todo); 27 return createdTodo.save(); 28 } 29 30 async updateTodo(id: string, todo: UpdateTodoDto): Promise<TodoDocument | null> { 31 return this.todoModel.findByIdAndUpdate(id, todo, {new: true}).exec(); 32 } 33 34 async markTodoComplete(id: string): Promise<TodoDocument | null> { 35 return this.updateTodo(id, { completed: true }); 36 } 37 38 async deleteTodo(id: string): Promise<TodoDocument | null> { 39 return this.todoModel.findByIdAndDelete(id).exec(); 40 } 41}
Explanation:
TodoService
: Contains methods for CRUD operations.findAll()
: Retrieves ToDo items, with an option to filter incomplete ones.findOne(id)
: Finds a ToDo item by its ID.createTodo()
: Creates a new ToDo item.updateTodo()
: Updates an existing ToDo item by its ID.markTodoComplete()
: Marks a ToDo item as completed.deleteTodo()
: Deletes a ToDo item by its ID.
The controller layer handles incoming HTTP requests and sends responses back to the client. We don't need to change much here except that the TodoService
is now asynchronous. This means that it can take some time for the Mongo requests to complete. We need to update the controller to handle asynchronous TodoService
calls!
Create a file src/todo/todo.controller.ts
and set up the controller:
TypeScript1import { Controller, Get, Post, Put, Delete, Param, Body, Query, NotFoundException } from '@nestjs/common'; 2import { TodoService } from './todo.service'; 3import { CreateTodoDto, UpdateTodoDto } from './dtos/todo.dto'; 4 5@Controller('todos') 6export class TodoController { 7 constructor(private readonly todoService: TodoService) {} 8 9 @Get() 10 async findAll(@Query('showIncomplete') showIncomplete: boolean) { 11 const todos = await this.todoService.findAll(showIncomplete); 12 return todos; 13 } 14 15 @Get(':id') 16 async findOne(@Param('id') id: string) { 17 const todo = await this.todoService.findOne(id); 18 19 if (!todo) { 20 throw new NotFoundException('Todo not found'); 21 } 22 23 return todo; 24 } 25 26 @Post() 27 async create(@Body() todo: CreateTodoDto) { 28 const newTodo = await this.todoService.createTodo(todo); 29 return newTodo; 30 } 31 32 @Put(':id') 33 async update(@Param('id') id: string, @Body() todo: UpdateTodoDto) { 34 const updatedTodo = await this.todoService.updateTodo(id, todo); 35 36 if (!updatedTodo) { 37 throw new NotFoundException('Todo not found'); 38 } 39 40 return updatedTodo; 41 } 42 43 @Put(':id/complete') 44 async complete(@Param('id') id: string) { 45 const completedTodo = await this.todoService.markTodoComplete(id); 46 47 if (!completedTodo) { 48 throw new NotFoundException('Todo not found'); 49 } 50 51 return completedTodo; 52 } 53 54 @Delete(':id') 55 async remove(@Param('id') id: string) { 56 await this.todoService.deleteTodo(id); 57 } 58}
Explanation:
TodoController
: Maps HTTP requests to service methods.findAll()
: Handles GET requests to fetch all ToDos, with a filter option.findOne(id)
: Handles GET requests to retrieve a ToDo item by ID.create()
: Handles POST requests to create a new ToDo item.update(id)
: Handles PUT requests to update a ToDo item by ID.complete(id)
: Handles PUT requests to mark a ToDo item as completed.remove(id)
: Handles DELETE requests to delete a ToDo item by ID.- Properly handles
NotFoundException
for non-existent ToDo items.
In this lesson, we introduced databases and MongoDB, set up MongoDB in the development environment, installed and configured Mongoose in a NestJS application, defined a schema for ToDo items, built the service layer for CRUD operations, implemented the controller to handle HTTP requests, and tested our integration using HTTP requests.
- Databases: Essential for storing and managing data.
- MongoDB: A flexible and scalable NoSQL database.
- Mongoose: Simplifies MongoDB interactions by providing schema validation and object mapping.
- ToDo Schema: Defines the structure and properties of ToDo items.
- Service Layer: Contains business logic for CRUD operations.
- Controller Layer: Handles HTTP requests and maps them to service methods.
- Testing: Validates the integrations and ensures the application's functionality.
Now that you have a working understanding of integrating MongoDB into a NestJS application, it's time to solidify your knowledge by working on practice exercises. These exercises will help reinforce everything you've learned and give you hands-on experience with the concepts. Happy coding!