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,
}
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();
}
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
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
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 thecontrollers
array and the imports, - Remove the reference to
AppService
in theproviders
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 {}
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
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();
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();
Generating Modules
To generate a NestJS module using the NestJS CLI run the code snippet below.
nest generate module blogs
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
Then run the code snippet below to generate the interface.
nest generate interface blogs
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;
}
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 ../..
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
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
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 {}
MongoDB(Mongoose).
Install mongoose by running,
npm install --save @nestjs/mongoose mongoose
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)],
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 itschemas
, - Inside the
schemas
folder, create a file and call itblogs.schema.ts
.
Then,
Firstly, you’ll have to,
- Import the
prop
decorator, theSchema
decorator, and theSchemaFactory
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 onSchemaFactory
that you imported earlier.
//blogs.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class Blog {}
export const BlogSchema = SchemaFactory.createForClass(Blog);
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);
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;
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);
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 theMongooseModule
. This takes in an array containing an object that defines aname
and aschema
property which should be set to yourBlog.name
and yourBlogSchema
respectively.
@Module({
imports: [
MongooseModule.forFeature([{ name: Blog.name, schema: BlogSchema }]),
],
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 theBlogsService
class, - Declare a
private
variable and call itblogModel
and assign a type ofModel<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 passBlog.name
as an argument.
constructor(
@InjectModel(Blog.name)
private blogModel: Model<BlogDocument>,
) {}
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';
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';
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) {}
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();
}
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);
}
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);
}
Controller Logic
@Get()
async getAllBlogs() {
return await this.service.getAllBlogs();
}
@Get(':id')
async getBlog(@Param('id') id: string) {
return await this.service.getBlog(id);
}
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);
}
Controller Logic
@Put(':id')
async updateBlog(
@Param('id')
id: string,
@Body()
blog: BlogsInterface,
) {
return await this.service.updateBlog(id, blog);
}
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);
}
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);
}
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"
}
{
"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"
}
{
"title": "Why You Should Follow me on Twitter",
"body": "Well, Because I asked nicely",
"category":"Random"
}
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"
}
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!
Top comments (0)