CRUD API using NestJS, Mongoose, MongoDB and Docker

In this article, we will set some CRUD API using:

  • NestJS (NodeJS Framework)
  • Mongoose (ODM that helps you to create a Schema based on the Model)
  • MongoDB (NoSQL Database)
  • Docker (Platform to deploy applications using containers)

GitHub Repository: github.com/FrancescoXX/nest-mongo-docker


Prerequisite: NodeJs installed, Docker (Docker desktop on windows/Mac Machines, Docker installed natively on Linux)

But you can follow way better if you:

  • Know how to run a "hello world" application using NestJS
  • Know what MongoDB Atlas is and how it works
  • Have an idea of what Mongoose is
  • Know what a Dockerfile and a Dockerfile is

NestJS

image.png

NestJs is a Node.js framework for building efficient and scalable applications, with full support to Typescript. It has a CLI you can use to generate projects and block easily and It uses express under the hood but has many interesting features like the building blocks:

  • Modules
  • Providers
  • Controllers
  • Services

Mongoose

image.png

Mongoose is an ODM (Object Data Modeling) javascript library for MongoDB and Node.

It is used to manage data relationships, uses Schemas and helps you to connect to a mongo db using mongo DRIVER


MongoDB

image.png

MongoDB is a NoSQL database. It stores documents and by using a Json-like object structure it's very easy to use with a Node application

In this example, we will use MongoDB Atlas, which is a cloud solution and it's perfect for an example. But of course, we could use it locally, even better if inside a docker container.


DOCKER

image.png

Docker is a platform to build run and share application using the idea of containers. If you want a brief introduction, here is a short video

IMAGE ALT TEXT HERE

youtu.be/eN_O4zd4D9o


Let's START!

Project Setup

Install NestJS CLI

NestJS comes with an easy to use CLI, that you can install globally (or locally) and it helps you to get started and use NestJS

npm install -g @nestjs/cli

Install dependencies

npm install mongoose  @nestjs/mongoose

Create a new nest project (a new folder will be created)

nest new nest-mongo-docker

Open the project with your favorite IDE. If you have VS Code, you can type

code nest-mongo-docker

To check if it's running, just type

npm run start

And check localhost:3000

image.png

If you see something like that, you can keep going :)

From the '/nest-mongo-docker' folder, using the nest CLI, we can generate files for the resource we want to create:

  • a controller
  • a service
  • a module
nest generate controller users
nest generate service users
nest generate module users

and let's create a user.module.ts file. From the command line, you can use

touch user.model.ts

Our project structure should look like this:

image.png


Model

in the users.model.ts, we can create a model for the resource we want to use. We will use a 'user' resource, with name, surname, and points, of the type string, string, and number respectively. Please note that here we are not using Typescript types, but plain JavaScript ones.

We don't need to add the "id" primary key in the schema, because it will be generated by Mongoose

import * as mongoose from 'mongoose';

export const UserSchema = new mongoose.Schema({
  name: { type: String, required: true },
  surname: { type: String, required: true },
  points: { type: Number, required: true },
});

export interface User extends mongoose.Document {
  id: string;
  name: string;
  surname: string;
  points: number;
}

Controller

Let's work on the Controller. NestJs forces us to create a solid structure for our HTTP requests, and it uses decorators for HTTP verbs.

We will create 5 endpoints

  • Get All Users
  • Get One user (passing the id in the url)
  • Create a new User (a Post request passing the new users value in the body)
  • Update a user (using the PATCH method, the id of the user in the url and the values we want to modify in the body)
  • Delete One User: To delete one user passing the id in the URL

in the users.controller.ts

import {
  Controller,
  Post,
  Body,
  Get,
  Param,
  Patch,
  Delete,
} from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  async createOneUser(
    @Body('name') name: string,
    @Body('surname') surname: string,
    @Body('points') points: number,
  ) {
    const generatedId = await this.usersService.createOneUser(
      name,
      surname,
      points,
    );
    return { id: generatedId };
  }

  @Get()
  getAllUsers() {
    return this.usersService.getAllUsers();
  }

  @Get(':id')
  getOneUser(@Param('id') userId: string) {
    return this.usersService.getOneUser(userId);
  }

  @Patch(':id')
  updateUser(
    @Param('id') userId: string,
    @Body('name') userName: string,
    @Body('surname') userSurname: string,
    @Body('points') userPoints: number,
  ) {
    this.usersService.updateUser(userId, userName, userSurname, userPoints);
    return null;
  }

  @Delete(':id')
  deleteUser(@Param('id') userId: string) {
    this.usersService.deleteUser(userId);
    return null;
  }
}

Service

We will use a Nest Service, which will be called by the controller functions.

Using the Mongoose library, we can easily work with objects in the Mongo Database

We can also import "NotFoundException" to have better error Handling in our application.

users.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { User } from './user.model';

@Injectable()
export class UsersService {
  constructor(@InjectModel('User') private readonly userModel: Model<User>) {}

  /**
   * Create One User
   *
   * @param name
   * @param surname
   * @param points
   */
  async createOneUser(name: string, surname: string, points: number) {
    const newUser = new this.userModel({
      name,
      surname,
      points,
    });
    const result = await newUser.save();
    return result.id as string;
  }

  /**
   * Get All Users
   */
  async getAllUsers() {
    const users = await this.userModel.find().exec();
    return users.map((user) => ({
      id: user.id,
      name: user.name,
      surname: user.surname,
      points: user.points,
    }));
  }

  /**
   * Get One User
   * @param userId
   */
  async getOneUser(userId: string) {
    const user = await this.findUser(userId);
    return {
      id: user.id,
      name: user.name,
      surname: user.surname,
      points: user.points,
    };
  }

  async updateUser(
    userId: string,
    name: string,
    surname: string,
    points: number,
  ) {
    const modUser = await this.findUser(userId);

    //Only modify Values passed
    if (name) modUser.name = name;
    if (surname) modUser.surname = surname;
    if (points) modUser.points = points;

    modUser.save();
  }

  async deleteUser(userId: string) {
    const result = await this.userModel.deleteOne({ _id: userId }).exec();
    if (result.n === 0) {
      throw new NotFoundException('Could not find user.');
    }
  }

  private async findUser(id: string): Promise<User> {
    let user: any;
    try {
      user = await this.userModel.findById(id).exec();
    } catch (error) {
      throw new NotFoundException('Could not find user.');
    }
    if (!user) {
      throw new NotFoundException('Could not find user.');
    }
    return user;
  }
}

Module

The UsersController and UsersService can be imported as a Module, and in this Module we can define some environment variables, to define the connection with the Mongo DB:

  • MONGO_ATLAS_USER
  • MONGO_ATLAS_PASSWORD
  • MONGO_ATLAS_DB

You can change this,m and they must match the user and password you have on The MongoDB ATLAS

image.png

in the users.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
  ],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

image.png in the app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    UsersModule,
    MongooseModule.forRoot(
      //Replace this line with the one Cluster > Connect > Connect your Application
      `mongodb+srv://${process.env.MONGO_ATLAS_USER}:${process.env.MONGO_ATLAS_PASSWORD}@cluster0.suflu.mongodb.net/${process.env.MONGO_ATLAS_DB}?retryWrites=true&w=majority`,
    ),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Remember to replace the line inside the forRoot method accordingly to your application.

To check that, go to Atlas DB site, Cluster > Connect > Connect your application, and copy the line below.

replace username, password and database using JavaScript Template literal. This is useful to pass the values from env variable instead of hardcoding them here.

image.png

ATLAS DB Setup

mongodb.com/cloud/atlas/lp/try2?utm_source=..

DOCKER

let's create 3 files to containerize our nest js application

Dockerfile

FROM node:14

EXPOSE 3000

# Use latest version of npm
RUN npm install npm@latest -g

# Install the nestJs CLI
RUN npm install -g @nestjs/cli

COPY package.json package-lock.json* ./

RUN npm install --no-optional && npm cache clean --force

# copy in our source code last, as it changes the most
WORKDIR /app

COPY . .

CMD [ "npm", "run", "start" ]

Please note the 9th line:

This is specific to NestJs, and we need to add this one in order to operate with the Nest CLI inside the container

the .dockerignore

node_modules
dist
.git
.vscode

The docker-compose.yml

version: "3.8"
services:
  nest_mongo_backend:
    container_name: nest_mongo_backend
    image: francescoxx/nest-mongo:0.0.3
    build:
      context: .
    ports:
      - "3000:3000"
    environment:
      - EXTERNAL_PORT=3000
      - MONGO_ATLAS_USER=francesco
      - MONGO_ATLAS_PASSWORD=<YOURPASSWORDHERE>
      - MONGO_ATLAS_DB=nest-mongo-docker-db

Replace with your password in MongoDB ATLAS.

You can find it on Atlas Site, under "Database Access"

image.png

Also replace the "image" value with something like:

docklerhubuser/nest-mongo:0.0.1

time to run our application

docker-compose up -d --build nest_mongo_backend

check if the app is up and running using

docker ps

image.png


POSTMAN / MONGODB ATLAS

image.png

We will use Postman, but you can use a whenever tool you want

let's make a GET request like this

image.png

our users are empty.

We can confirm this by checking on Mongo Atlas

image.png

Click on "Collections"

image.png

As you can see, the users list is empty

Let's create 3 users with a POST request, using POSTMAN. Here is an example of one of the 3

image.png

Let's check again if we have new users

image.png

Let's check on Atlas

image.png

Here you can see the 3 users

Let's try the Get One, which is used to get just one user, using an id

Remember to change your id with the one you can see with the get all request!

image.png

Let's try to UPDATE one filed of the first user, using a PATCh Request

image.png

Let's check it again using the Get (One) request

image.png

Last, let's try to delete the last user (check its id using the get all)

image.png

And as you can see, the user has been correctly deleted!

GitHub Repository: github.com/FrancescoXX/nest-mongo-docker

If this article has been interesting, consider becoming a GitHub Sponsor: github.com/sponsors/FrancescoXX

No Comments Yet