DEV Community

Cover image for The ABCs of NestJS: A Beginner’s guide with MongoDB(Mongoose).
davidekete
davidekete

Posted on • Updated on • Originally published at davidekete.hashnode.dev

The ABCs of NestJS: A Beginner’s guide with MongoDB(Mongoose).

What is NestJS?

NestJS is a modern NodeJS framework that makes use of popular NodeJS frameworks such as Express and Fastify under the hood. NestJS was largely inspired by Angular, and as a result, it employs an Angular-style module system. NestJS is written in TypeScript, although it also supports native JavaScript.

Prerequisites

To follow this tutorial, you must meet the following requirements

  • Competence in PostMan or any other API testing tool.
  • Basic Knowledge of NodeJS and Express apps.
  • Basic knowledge of TypeScript.
  • Competence in MongoDB(Mongoose).

The following should be installed on your system

  • NodeJS v.14 and above.
  • Visual Studio Code(Recommended) or any other IDE.
  • PostMan or any other API testing Tool.

Common Terminologies used in NestJS;

Here are some of the most regularly used terms in NestJS that you'll encounter a lot in this article.

Interfaces

An interface is a type definition. As a result, it is utilized as a type checker/enforcer in functions, classes, etc.

interface humanInterface{
  name:string;
  gender:string;
  age:number;
}

const kevin: humanInterface={
  name:'Kevin Sunders',
  gender:'Male',
  age: 25,
}
Enter fullscreen mode Exit fullscreen mode

The humanInterface above performs strict type checking on the kevin object. Typescript would throw an error if you added another field or changed the type of any of the object properties.

Controllers

Controllers are in charge of receiving incoming requests and responding to the client. A controller collaborates with its associated service.

Services

A service is a provider that stores and retrieves data and is used with its corresponding controller.

Decorators

A decorator is a function-returning expression that accepts a target, name, and property descriptor as optional arguments. Decorators are written as @decorator-name. They are usually attached to class declarations, methods, and parameters.

@Get()
   getAll(): Model[] {
    return this.testService.getAll();
  }
Enter fullscreen mode Exit fullscreen mode

The @Get decorator above marks the code block below it as a GET request. More about that later on.

Module

A module is a part of a program that handles a particular task. A module in NestJS is marked by annotating a class annotated with the @Module() decorator. Nest uses the metadata provided by the @Module() decorator to organize the application structure.

Installing the CLI

To get started you’ll have to install the NestJS CLI ****with npm. You can skip this step if you already have the NestJS CLI installed on your system.

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

This code block above will install the nest CLI globally on your system.

Creating a new project

To generate a new project run nest new followed by your desired project name. For this article, we’ll be writing a simple blog API with CRUD functionality while adhering to RESTful standards.

nest new Blog-Api
Enter fullscreen mode Exit fullscreen mode

This command will prompt you to select a package manager, choose npm.

This will then scaffold the entire project structure with a test API endpoint whose port is set to 3000 by default. You can test it at http://localhost:3000 after running the npm run start:dev command which will start the server in watch mode similar to what nodemon does in express apps.

After testing the endpoint, you’ll need to delete some of the default files because you won’t be needing them anymore. To do this;

  • open the src folder and inside,
  • delete app.controller.spec.ts,
  • delete app.controller.ts,
  • delete app.service.ts,
  • Open app.module.ts,
  • Remove the reference to AppController in the controllers array and the imports,
  • Remove the reference to AppService in the providers array and the imports.

You might also need to change the README.md to meet your specifications.

Your app.module.ts file should look like this,

//app.module.ts

import { Module } from '@nestjs/common';

@Module({
  imports: [],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Environmental Variables

As good practice, some sensitive information in your code shouldn't be made public. For example your PORT and your MongoDB URI.

Let's fix this in your code.

On your terminal run

npm i dotenv
Enter fullscreen mode Exit fullscreen mode

Then create a .env file in your directory and add it to your .gitignore file. Store your PORT variable, you will also have to store your MongoDB URI later in the same place. Now replace the exposed PORT in your main.ts file. To do this, import the dotenv package and call the .config() method on it.

import * as dotenv from 'dotenv';
dotenv.config();
Enter fullscreen mode Exit fullscreen mode

This should be your main.ts file after you follow the steps above.

//main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as dotenv from 'dotenv';
dotenv.config();

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Generating Modules

To generate a NestJS module using the NestJS CLI run the code snippet below.

nest generate module blogs
Enter fullscreen mode Exit fullscreen mode

This command creates a blogs folder that contains a blogs.module.ts file and registers BlogsModule in your app.module.ts file.

Generating Interfaces

Let’s generate an interface using the NestJS CLI to do the type checking for the object that will represent your blog posts. To achieve this you have to first cd into the blogs folder because it is recommended that they be stored near the domain objects to which they are associated.

cd src/blogs
Enter fullscreen mode Exit fullscreen mode

Then run the code snippet below to generate the interface.

nest generate interface blogs
Enter fullscreen mode Exit fullscreen mode

this creates a blogs.interface.ts file. This is where we will define our interface. we’ll name the interface BlogsInterface.

export interface BlogsInterface {
  title: string;
  body: string;
  category: string;
  dateCreated: Date;
}
Enter fullscreen mode Exit fullscreen mode

before running any more commands on your terminal, remember to cd out of the src folder and back into your root folder by running

cd ../..
Enter fullscreen mode Exit fullscreen mode

Generating Services & Controllers

You’ll need to generate a service class to store and retrieve data and handle all the logic and a controller class to handle all incoming requests and outgoing responses.

Service

To generate a service run the command below,

nest generate service blogs
Enter fullscreen mode Exit fullscreen mode

This command creates two files the blogs.service.spec.ts and the blogs.service.ts and registers the service in the providers array in the blogs.module.ts.

Controller

To generate a controller run the command below,

nest generate controller blogs
Enter fullscreen mode Exit fullscreen mode

This command creates two files the blogs.controller.spec.ts and the blogs.controller.ts and registers the controller in the controllers array in the blogs.module.ts.

With these your blogs structure is almost complete, you just need to make the BlogsService accessible to other parts of your program. You can achieve this by creating an exports array in the blogs.module.ts file and registering the BlogsService in that array.

//blogs.module.ts

import { Module } from '@nestjs/common';
import { BlogsService } from './blogs.service';
import { BlogsController } from './blogs.controller';

@Module({
  providers: [BlogsService],
  controllers: [BlogsController],
  exports: [BlogsService],
})
export class BlogsModule {}
Enter fullscreen mode Exit fullscreen mode

MongoDB(Mongoose).

Install mongoose by running,

npm install --save @nestjs/mongoose mongoose
Enter fullscreen mode Exit fullscreen mode

After the installation, import {MongooseModule} from '@nestjs/mongoose’ into your app.module.ts file. Then grab your MongoDB URI and store it in your .env file. Repeat the steps to import dotenv in the app.module.ts file. Then in the imports array call the .forRoot() method which takes your MongoDB URI as an argument on the MongooseModule. Similar to the mongoose.connect() in regular express apps.

@Module({
  imports: [BlogsModule, MongooseModule.forRoot(process.env.MONGODB_URI)],
Enter fullscreen mode Exit fullscreen mode

Creating a Schema.

Let’s create a schema to define the shape of the blogs in our collection. To do this,

  • Create a folder inside your blogs folder, name it schemas,
  • Inside the schemas folder, create a file and call it blogs.schema.ts.

Then,

Firstly, you’ll have to,

  • Import the prop decorator, the Schema decorator, and the SchemaFactory from @nestjs/mongoose,
  • Create a class Blog and export it,
  • Turn the class into a Schema by placing the @Schema() decorator above the class,
  • Create a constant BlogSchema, assign the return value of calling the .createForClass(Blog) with the name of your class as an argument on SchemaFactory that you imported earlier.
//blogs.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';

@Schema()
export class Blog {}

export const BlogSchema = SchemaFactory.createForClass(Blog);
Enter fullscreen mode Exit fullscreen mode

Then you’ll need to define the properties of the Schema.

To define a property in the schema you’ll need to mark each of them with the @prop() decorator. The @prop decorator accepts an options object or a complex type declaration. The complex type declarations could be arrays and nested object type declarations.

//blogs.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';

@Schema()
export class Blog {
  @Prop({ required: true })
  title: string;

  @Prop({ required: true })
  body: string;

  @Prop({ required: true })
  category: string;

  @Prop({ required: true })
  dateCreated: Date;
}

export const BlogSchema = SchemaFactory.createForClass(Blog);
Enter fullscreen mode Exit fullscreen mode

Next import { Document } from 'mongoose'.

Then create a union type with the Schema class and the imported Document. Like so,

//blogs.schema.ts

import { Document } from 'mongoose';

export type BlogDocument = Blog & Document;
Enter fullscreen mode Exit fullscreen mode

Your final blogs.schema.ts file should look like this,

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type BlogDocument = Blog & Document;

@Schema()
export class Blog {
  @Prop({ required: true })
  title: string;

  @Prop({ required: true })
  body: string;

  @Prop({ required: true })
  category: string;

  @Prop({ required: true })
  dateCreated: Date;
}

export const BlogSchema = SchemaFactory.createForClass(Blog);
Enter fullscreen mode Exit fullscreen mode

Registering Schema

You’ll need to import everything into your blogs.module.ts file. To achieve this you’ll need to,

  • Import {MongooseModule} from '@nestjs/mongoose’,
  • Import {Blog, BlogSchema} from './schemas/blogs.schema'
  • Create an imports array inside the @module decorator
  • Call the .forFeature() method on the MongooseModule. This takes in an array containing an object that defines a name and a schema property which should be set to your Blog.name and your BlogSchema respectively.
@Module({
  imports: [
    MongooseModule.forFeature([{ name: Blog.name, schema: BlogSchema }]),
  ],
Enter fullscreen mode Exit fullscreen mode

Injecting Model

You’ll need to inject the Blog model into the blogs.service.ts using the @InjectModel() decorator. To achieve this you’ll have to

  • import { Model } from 'mongoose',
  • import { InjectModel } from '@nestjs/mongoose',
  • Import {Blog, BlogDocument} from './schemas/blogs.schema’,
  • Create a constructor inside the BlogsService class,
  • Declare a private variable and call it blogModel and assign a type of Model<BlogDocument> to it. All mongoose methods will be called on this variable.

Recall that, BlogDocument is the union type of the Blog class and the Mongoose Model that you created earlier. It is used as the generic type for your variable.

  • Decorate blogModel with @InjectModel() and pass Blog.name as an argument.
constructor(
    @InjectModel(Blog.name)
    private blogModel: Model<BlogDocument>,
  ) {}
Enter fullscreen mode Exit fullscreen mode

How Routing Works

By now you must have noticed that the @Controller decorator has the string 'blogs' passed into it. This means that the controller will send all responses and handle all requests made on http://localhost/3000/blogs.

Next up you’ll implement the service and controller logic.

Service and Controller Logic.

It's finally time to implement your CRUD functionality.

Before we get started you’ll need to set up your controller. Start by importing some HTTP method decorators into your controller.

//blogs.controller.ts

import {
  Controller,
  Body,
  Delete,
  Get,
  Post,
  Put,
  Param,
} from '@nestjs/common';
Enter fullscreen mode Exit fullscreen mode

Then, you’ll need to import the service and register it so you can be able to access it and import the interface for type-checking.

//blogs.controller.ts

import { BlogsInterface } from './blogs.interface';
import { BlogsService } from './blogs.service';
Enter fullscreen mode Exit fullscreen mode

To register your service create a constructor inside the BlogsController class and declare a private readonly variable service and set its type to BlogsService.

constructor(private readonly service: BlogsService) {}
Enter fullscreen mode Exit fullscreen mode

Now that you’re all set up, let’s get started.

Create

Service Logic

Import { BlogsInterface } from './blogs.interface' and add an async function to the BlogsService class called createBlog, which will take one parameter blog, with its type as BlogInterface, and its return type as a Promise with a generic <Blog> type.

async createBlog(blog: BlogsInterface): Promise<Blog> {
    return await new this.blogModel({
      ...blog,
      dateCreated: new Date(),
    }).save();
  }
Enter fullscreen mode Exit fullscreen mode

Controller Logic

In your BlogsController class add an async function to the class. Call it createBlog and mark it with the @Post decorator which defines it as a POST request.createBlog takes one parameter blog, with its type as BlogInterface. Mark the parameter with @Body decorator which extracts the entire body object from the req object and populates the decorated parameter with the value of body.

@Post()
  async createBlog(
    @Body()
    blog: BlogsInterface,
  ) {
    return await this.service.createBlog(blog);
  }
Enter fullscreen mode Exit fullscreen mode

Read

Add two async methods, One to return a single blog post and the second to return all the blog posts.

Service Logic

async getAllBlogs(): Promise<Blog[]> {
    return await this.blogModel.find().exec();
  }

  async getBlog(id: string): Promise<Blog> {
    return await this.blogModel.findById(id);
  }
Enter fullscreen mode Exit fullscreen mode

Controller Logic

  @Get()
  async getAllBlogs() {
    return await this.service.getAllBlogs();
  }

  @Get(':id')
  async getBlog(@Param('id') id: string) {
    return await this.service.getBlog(id);
  }
Enter fullscreen mode Exit fullscreen mode

The async functions are marked with the @Get decorator which defines it as a GET request.

The second async function’s decorator has an argument ':id'. Which is what you’ll pass into the @Param decorator. The parameter is marked with the @Param('id') which extracts the params property from the req object and populates the decorated parameter with the value of params.

Update

Let’s implement the logic for the PUT request.

Service Logic

async updateBlog(id: string, body: BlogsInterface): Promise<Blog> {
    return await this.blogModel.findByIdAndUpdate(id, body);
  }
Enter fullscreen mode Exit fullscreen mode

Controller Logic

@Put(':id')
  async updateBlog(
    @Param('id')
    id: string,
    @Body()
    blog: BlogsInterface,
  ) {
    return await this.service.updateBlog(id, blog);
  } 
Enter fullscreen mode Exit fullscreen mode

The async function’s second parameter is marked with the @Body() decorator which extracts the entire body object from the req object and populates the decorated parameter with the value of body.

Delete

Let’s implement the logic for delete requests.

Service Logic

async deleteBlog(id: string): Promise<void> {
    return await this.blogModel.findByIdAndDelete(id);
  }
Enter fullscreen mode Exit fullscreen mode

The Promise generic type is void because a Delete request returns an empty promise.

Controller Logic

@Delete(':id')
  async deleteBlog(@Param('id') id: string) {
    return await this.service.deleteBlog(id);
  }
Enter fullscreen mode Exit fullscreen mode

Testing the API

To test this API, you should use an API testing tool. For this article, I’ll be using a popular API testing tool called Postman. I’ll be using random data about popular topics to test.

Create

Make a POST request to http://localhost/3000/blogs with the following JSON objects, this will add all the data to your database.

{
  "title": "jeen-yuhs",
  "body": "The life of superstar rapper Kanye West is currently streaming on Netflix - and according to our jeen-yuhs review, it's a fascinating watch. -credit:Radio Times",
  "category":"Music"
}

Enter fullscreen mode Exit fullscreen mode
{
  "title": "Why You Should Always Wash Your Hands",
  "body": "Germs from unwashed hands can be transferred to other objects, like handrails, tabletops, or toys, and then transferred to another person's hands.-credit cdc.gov",
  "category":"Health"
}

Enter fullscreen mode Exit fullscreen mode
{
  "title": "Why You Should Follow me on Twitter",
  "body": "Well, Because I asked nicely",
  "category":"Random"
}
Enter fullscreen mode Exit fullscreen mode

You should get a 201 response and the created blog with a date and an _id added.

Read

Make a GET request to http://localhost/3000/blogs. This should return a

200 response with an array of all the data you previously added. Copy the _id property of one of the array objects.

Make another GET request to http://localhost/3000/blogs/id with the previously copied id. This should return a 200 response with the data of the object whose id was used to make the request.

Update

Make a PUT request to http://localhost/3000/blogs/id with the data below. The id should be replaced with the one you copied earlier. This should return a 200 response and updates the object bearing the id behind the scenes. if you run another GET request you should get the updated object.

{
  "title": "why you Should Cut your Nails",
  "body": "It's important to trim your nails regularly. Nail trimming together with manicures makes your nails look well-groomed, neat, and tidy.- credit:WebMD",
  "category":"Health"
}
Enter fullscreen mode Exit fullscreen mode

Delete

Make a DELETE request to http://localhost/3000/blogs/id.This should return a 200 response and deletes the object bearing the id behind the scenes. if you run another GET request you won’t see the deleted object.

Conclusion

So we’re finally at the end of this article. Let’s recap what you’ve covered.

  • What NestJS is,
  • Terminologies in NestJS,
  • Creating a NestJS app,
  • Integrating MongoDB into a NestJS app,
  • Manipulating and NestJS app,

That’s quite a lot, congratulations on making it this far.

You can find the code on github.

Good luck on your NestJS journey!

Discussion (0)