Codementor Events

REST api with mongodb and nest.js

Published Mar 21, 2018Last updated Sep 17, 2018

alt
Image from nest.js official website

Nest.js is a js framework that is built with typescript and does an extra ordinary job to give developers a stable base for building server side application. It has an extremely modular architecture that will allow you to be flexible but very robust at the same time. At times, their recommended way of writing app may seem a bit tedious but trust me, it pays off in the long run in maintainability and readability. With that in mind, I hope you'll bear with me through this post and come to the same conclusion when we're done building our app.

A few assumptions about you

If you're reading this post, I'm going to assume you're familiar with node.js, basic typescript and javascript in general. However, if you're not familiar with those or some things don't make sense to you, feel free to give it a read anyways and let me know how I can help you understand it better.

What we're going to build

With this post, we're setting on a journey together to build a full-fledged app where users can create posts and as new posts are being made users can see the posts in a realtime feed. I'm calling it parley but I'm not married to it so if you have a better name, let me know 😃 As first step, we will build the server side REST api with nest.js and work our way into the client side app and realtime features.

Tools needed to work along with this post

If you're going to work along with the tutorial and build the api on your local machine, you only need node.js and npm installed on your local machine. If you want to use local mongodb, make sure you have it installed as well. Otherwise, you can use external providers as well such as atlas, mlab or compose.

Getting started

Nest.js provides a solid starter base for you so we'll start from there to avoid initial bootstraping. Run the following commands to get the starter template:

$ git clone https://github.com/nestjs/typescript-starter.git parley
$ cd parley
$ npm install
$ npm run start:watch

You can now see the app in action by going to http://localhost:3000 on your browser. Now open up the directory in your favorite code editor (mine is vscode right now) and let's dive in and start coding.

Get connected to mongodb

In real world apps, we need a persistent data layer and I'm opting in for mongodb with mogoose for this one but you can use any data layer of your choice with nest.js. They have great documentation for the most popular ones like graphql, sequelize etc. This is our directory structure for the database module:

src
 |--database
      |--database.module.ts
      |--database.providers.ts

Now add the following code in your database.providers.ts file:

import * as mongoose from 'mongoose';
import { DB_PROVIDER } from '../constants'; 

export const databaseProviders = [
    {
        provide: DB_PROVIDER,
        useFactory: async () => {
            (mongoose as any).Promise = global.Promise;
            return await mongoose.connect('mongodb://localhost:27017/parley');
        },
    },
];

You'll notice how we're importing { DB_PROVIDER } from a file that doesn't yet exist so let's create the constants.ts file in the src/ directory and put the following code in it:

export const DB_PROVIDER = 'DbConnectionToken';

The dependency injection in nest.js uses string names and it's recommended to store your provider names in a separate file and use constant variables instead of magic strings. You can read more about it on the official doc.

Also, we're using mongoose as our ODM but we haven't installed it yet so let's do that real quick:

$ npm i --save mongoose
$ npm i --save-dev @types/mongoose

We're using mongodb://localhost:27017/parley as our mongodb address, assuming you have mongodb installed locally and have a database named parley in your db. If you're using an external db provider, use that db url instead.

Let's define our database module in the database.module.ts file :

import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
    components: [...databaseProviders],
    exports: [...databaseProviders],
})
export class DatabaseModule { };

That's all. Now we can pull in the database module in any other modules and use mongoose to connection object to run queries on our db.

Posts module

We will design our app from a entity based modular approach. We will start by creating our first module directory inside the src directory. With the post module our directory structure will look like this:

src
 |--posts
    |--dto
          |--create-post.dto.ts
      |--interfaces
          |--post.interface.ts
      |--posts.controller.ts
      |--posts.module.ts
      |--posts.providers.ts
      |--posts.schema.ts
      |--posts.service.ts

Go ahead and create the files and folders shown above. Let's first code up the create-post.dto.ts file. This file defines the data structure that api endpoints will expect. When creating a new post we expect the http request body to contain a title, a content and a userId. Ideally, the client should not be able to set the userId of a post and it should automatically be set from the logged in user but we will have to wait on that until we implement authentication feature on our app. So, for now, this is how it looks like:

export class CreatePostDto {
    readonly title: string;
    readonly content: string;
    readonly userId: string;
}

Now let's create our post interface in the post.interface.ts file that will be used as the post entity model in our typescript code. Each post should contain a title, a content and a userId :

import { Document } from 'mongoose';

export interface Post extends Document {
    readonly title: string;
    readonly content: string;
    readonly userId: string;
}

With those two files created, we're now ready to write our post service in the posts.service.ts file:

import { Model } from 'mongoose';
import { Component, Inject } from '@nestjs/common';

import { Post } from './interfaces/post.interface';
import { CreatePostDto } from './dto/create-post.dto';
import { POST_MODEL_PROVIDER } from '../constants';

@Component()
export class PostsService {
    constructor(
        @Inject(POST_MODEL_PROVIDER) private readonly postModel: Model<Post>) { }

    async create(createPostDto: CreatePostDto): Promise<Post> {
        const createdPost = new this.postModel(createPostDto);
        return await createdPost.save();
    }

    async findAll(): Promise<Post[]> {
        return await this.postModel.find().exec();
    }
}

Let's review this file a little closer. First we're importing the post interface and the create post dto that we've created earlier. Then, we're importing the POST_MODEL_PROVIDER constant from the constants.ts file but we haven't added the variable in that file yet. So let's add that:

export const DB_PROVIDER = 'DbConnectionToken';

export const POST_MODEL_PROVIDER = 'PostModelToken';

In our PostService component class constructor, we inject the post model which will be injected by nest.js dependency injection. However, we haven't created the provider for it yet. So let's create the provider in the posts.providers.ts file:

import { Connection } from 'mongoose';

import { PostSchema } from './posts.schema';
import { POST_MODEL_PROVIDER, DB_PROVIDER } from '../constants';

export const postsProviders = [
    {
        provide: POST_MODEL_PROVIDER,
        useFactory: (connection: Connection) => connection.model('Post', PostSchema),
        inject: [DB_PROVIDER],
    },
];

In the provider, we're basically building a mongoose model and injecting the model as a factory into nest.js dependency container so that later on, we can pull the model out of the container anywhere in our app. To create the model, we have to define and attach a schema. Let's define the schema in the posts.schema.ts file:

import * as mongoose from 'mongoose';

export const PostSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    content: {
        type: String,
        required: false,
    },
    userId: {
        type: mongoose.SchemaTypes.ObjectId,
        required: true,
    }
});

Ok, let's loop back to our provider file. We pull out the db connection provider from the container and build a mongoose model then store the model as a provider in the container under the POST_MODEL_PROVIDER name.

So that explains how the posts model provider can be injected in the posts service constructor. Now, back to the posts service file. The first method of that class is an async create method that accepts one paramater and returns a promise. The parameter itself is the post-create dto object. So if we try to pass anything that isn't allowed by the dto schema, it will be filtered out. This is makes it maintaining data integrity very easy and declarative. Inside the create method we simply instantiate a new post model and save it in the database.

The other method in that class is findAll that simply queries all posts and returns the entries from the db.

So far, we've handled the data layer of the posts. Now we're gonna hook up the data with nest.js controller to make them accessible through REST api endpoints. For that, we start with the controller.

A controller in nest.js is a simple class that uses decorators to define GET/POST/PUT etc. http endpoints of the app's api. Here's how our posts.controller.ts file looks like:

import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { PostsService } from './posts.service';
import { Post as PostInterface } from './interfaces/post.interface';

@Controller('posts')
export class PostsController {
    constructor(private readonly postsService: PostsService) { }

    @Post()
    async create(@Body() createPostDto: CreatePostDto) {
        this.postsService.create(createPostDto);
    }

    @Get()
    async findAll(): Promise<PostInterface[]> {
        return this.postsService.findAll();
    }
}

The @Controller decorator accepts one parameter which defines the endpoint that the controller is associated with. In our case, we pass 'posts' so, we can make api requests to http://localhost:3000/posts for post related communications. You can pass anything and that will be used as the endpoint base.

Nest.js has Get and Post decorators for GET and POST api endpoints but since our posts also has the interface defined as Post we need to change the import name of at least one of the variables. Which is why I've changed the Post interface variable name into PostInterface when importing it. The controller constructor injects postsService instance.

We have a create method decorated with @Post decorator that ensures that any http POST request coming to our app on /posts endpoint will fire that create method. The first parameter of the method is decorated with the @Body decorator that gives you access to the body of the request (it is equivalent to req.body if you're familiar with express). The best part about using nest.js decorators is they provide a lot of convenience such as json body parsing. You don't need to configure a body parsing middleware for accepting json input from client calls, nest.js decorators will do it for you. That's why we can expect the body to match our create-post dto. Within the method we simply call the create method of the postsService.

The next method findAll is defined in a similar fashion that doesn't accept any parameters and returns all posts queried through the postsService.

Phew! That's a lot of work for one module but I hope it all made sense and as our module grows with more features, you'll notice how much easier it is to sprinkle those on top of this layout. Finally, we need to tie it all up and define our posts module in the posts.module.ts file like below:

import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { postsProviders } from './posts.providers';
import { DatabaseModule } from '../database/database.module';

@Module({
    imports: [DatabaseModule],
    controllers: [PostsController],
    components: [
        PostsService,
        ...postsProviders,
    ],
})
export class PostsModule { }

You are probably noticing how nice the module structure of nest.js can be where everything is so nicely decoupled and encapsulated. Now we can simply load this module in our app.module.ts file:

......
import { AppController } from './app.controller';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [PostsModule],
.....

All Done. Let's go check it out.

Testing our REST api

I use postman for testing my api endpoints but you can test the first endpoint by simply going to http://localhost:3000/posts/ on your browser and you should see [] output. Since we have no entries in the posts collection of our database, it's returning an empty array. Now you can create a post entry in the database manually or do it through the api endpoint we just created.

If you wanna do it real quick, just copy paste the following curl command and paste it on your terminal :

curl -X POST \
  http://localhost:3000/posts/ \
  -H 'content-type: application/json' \
  -d '{"title": "this is post one", "content": "You'\''ve gotta write clearer so you can be read when you'\''re dead", "userId": "5ab25d50740ce24b63cc9c83"}'

Or if you wanna use postman like me, create a new POST request to http://localhost:3000/posts. Then add a new header with name Content-Type and value application/json. Now go to the body tab and set body type raw and paste the following content:

{"title": "this is post one", "content": "You've gotta write clearer so you can be read when you're dead", "userId": "5ab25d50740ce24b63cc9c83"}

Notice how I'm giving an arbitrary string value to the userId property? That's because we don't have user entries in the db yet but mongoose schema expects the userId to be of type ObjectID so we're going to hard code it for now. It will later be replaced with our actual logged in user's ID. Now press Send and go back to your browser on http://localhost:3000/posts and you will get a new response printed like :

[
  {
    _id: "5ab2c497b8bfa11f47c4c38a",
    title: "this is post one",
    content: "You've gotta write clearer so you can be read when you're dead",
    userId: "5ab25d50740ce24b63cc9c83",
    __v: 0
  }
]

Congrats!! You've just created your first REST api on nest.js and mongodb. Pat yourself on the back or ask a colleague to do it for you, you've earned it!

What's next?

We've built a very robust but quite useless system so far. So up next, we're going to build a shiny new frontend app to interact with our api. I will update this post with the links to the posts where I walk you through the process of building that frontend app.

In the meantime, play around with the api, create new posts and see them come up on the /posts endpoint of our api.

Discover and read more posts from Foysal
get started
post comments14Replies
Ankur Gupta
5 years ago

Hi,

Super useful article.

Getting this error always there is something with provider. Tried with different started apps. Same error may be you can help.

Nest cannot export a provider/module that is not a part of the currently processed module (PostsModule). Please verify whether the exported PostsService is available in this particular context.

Also Component is not supported now. So may be update the article it will help people like me.

Thanks in Advance
Ankur

Max Pax
5 years ago

Did you try to replace @Component with @Injectable and also for module.ts to replace “components” with “providers” ?

dibeesh
6 years ago

Any git repository to get this sample code

Foysal
6 years ago

I swear I had a git repo linked on this post but now it looks like it’s not there lol. I’ll try to find it on my github but if I can’t, will put it up and link to it on the post.

dibeesh
6 years ago

Thank you. Its a great tutorial to begin with Nest.js…You are awesome

Rodrick Makore
6 years ago

Thank you for a great post… Notice the ‘DB_CONNECTION’ error in post. I changed my code to use DB_PROVIDER instead and was successful.

Foysal
6 years ago

Hey Rodrick, great catch. I updated the post to fix that error. Thank you.

Show more replies