Building Scalable APIs With NestJS and MongoDB

Building Scalable APIs With NestJS and MongoDB

With the help of GraphQL
Ferenc Almasi • Last updated 2021 November 11 • Read time 17 min read
Learn how you can leverage the power of NestJS combined with GraphQL and MongoDB to create highly scalable APIs for your Nest application.
  • twitter
  • facebook

Building scalable server-side applications that are both easy to maintain and read, can be a non-trivial task when it comes to large-scale applications. It requires a robust, but flexible project structure with consistent code, otherwise, we may end up with a project that is hard to extend and manage.

Because of these reasons, we will be looking into a progressive Node.js framework called NestJS for building scalable server-side applications.


Why Nest?

So why Nest? What is in it for us? NestJS — as we will see — is a highly modular Node.js framework for building modern backend applications. It combines multiple programming paradigms into a predefined project structure, unlike many other backend frameworks.

This eliminates the need to start from scratch and come up with your own setup and project structure. On the other hand, if you feel uncomfortable with how files are organized, you can always rearrange your modules until it feels right.

It comes with TypeScript, but you also have the option to use vanilla JavaScript. Its architecture is heavily inspired by Angular, so if you have hands-on experience with that, you will quickly get a grasp of it.


What Are We Building?

To stay true to the nature of Nest, we are going to build a CRUD application for querying, creating, modifying, and removing entries about cats through an API.

We will also look into how you can integrate GraphQL for writing better queries, and how you can connect all of this together to a MongoDB. Without any further ado, let’s jump into it.

The final project structure of our NestJS application
The final project structure of our app
Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

The Building Blocks of Nest

First, let’s look into what are the building blocks of Nest to get more familiar with the framework. It is built around modules that are managed by controllers and services.

Modules

Modules are a core part of NestJS. The main job of a module is to split your codebase into smaller, more manageable pieces. Each module is made up of controllers, services, resolvers, or other modules. Even the root of your application uses a module.

Copied to clipboard!
@Module({})
export class AppModule {}
app.module.ts
The root of a NestJS application

Although you could build your whole app around one module, it is not something you would want to do if you are thinking in large-scale. And speaking of large-scale, if you are building a small application, then NestJS might be overkill for you.

Modules in NestJS

Controllers

Controllers in Nest are responsible for handling incoming HTTP requests. Whenever a user hits one of your API’s endpoints, the request goes through one of the methods of a controller to send the appropriate response.

Copied to clipboard!
import { Controller } from '@nestjs/common';

@Controller()
export class AppController {}
app.controller.ts

Since we are going to work with GraphQL, we will be using a resolver instead, but the behavior will be the same.

Controllers in NestJS

Services

While you could technically start building applications with the two above, to further break things down, NestJS uses services for holding most of the business logic associated with your endpoints. That way, you essentially only return the methods of services from your controllers, and you avoid writing unnecessary logic for them. Instead, they will be part of a service.

Copied to clipboard!
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {}
app.service.ts

This is the main purpose of a service: to extract complex business logic from controllers. All of these building blocks are put together by NestJS’ dependency injection.

Services in NestJS

Setting Up the Project

To get our hands dirty, let’s quickly set up a new NestJS project. Luckily for us, NestJS provides a handy CLI that can be used to pre-generate new projects. I highly recommend using it. You can get it installed on your machine by running:

npm i -g @nestjs/cli

And with the CLI installed, you can bootstrap a new project using the nest new command through your terminal:

nest new nest-mongo-graphql

where the name after the new keyword references a folder name. During the scaffolding, Nest will ask you whether you want to use Node.js or Yarn as the packager manager. For this tutorial, we are going with Node.

Choose package manager when scaffolding NestJS project

After the installation has finished, open up your folder, and let’s discover what NestJS provides us as a starting point. What we are interested in, is the src folder. By default, it comes with 5 different files:

main.ts

Copied to clipboard!
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}

bootstrap();
main.ts

Your main.ts file is what bootstraps the whole NestJS application. This is your entry file. It calls the core NestFactory function to create a Nest application instance from a module that you pass to it. Then it starts up a server at port 3000 to listen for HTTP requests.

app.module.ts

Copied to clipboard!
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}
app.module.ts

Your app.module.ts is the root module of the app that is imported from main.ts, and bootstrapped with NestFactory.create. It imports a controller and a service, the two other building blocks that we’ve discussed earlier. Nest makes them available to the module by using the @Module decorator.

app.controller.ts

Copied to clipboard!
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get()
    getHello(): string {
        return this.appService.getHello();
    }
}
app.controller.ts

Note that NestJS makes heavy use of decorators. By default, Nest comes with a basic controller that has a single method, which is using a method from app.service.ts. Remember that we’ve said that Nest encourages to hand any business logic from controllers over to services.

app.service.ts

Copied to clipboard!
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
    getHello(): string {
        return 'Hello World!';
    }
}
app.service.ts

Looking at the service, all it does is returning a string that reads “Hello World!”. If you start up your dev server using

npm run start:dev

and visit localhost:3000, then you will see this message displayed. It is coming from this service, that is called from the controller, that is called from your main app module, which is bootstrapped from main.ts.

You will also have an app.controller.spec.ts file with a basic set of test cases to get you started if you want to test your modules.


Creating Your Very First Module

However, we don’t want to work with the built-in modules, we want to create our own stuff. To generate modules, you can use the generate command of Nest’s CLI:

nest generate module cat

This will generate a new folder for you — called “cat” — under your src folder. It will also create a cat.module.ts for you inside this folder. Extend the generated code with the following:

Copied to clipboard!
import { Module } from '@nestjs/common';
import { CatService } from './cat.service';
import { CatResolver } from './cat.resolver';

@Module({
    providers: [CatService, CatResolver],
})
export class CatModule {}
cat.module.ts

We also need a new service, and this time, a resolver instead of a controller. Note that you pass resolvers to the providers array, instead of controllers, like we had for app.module.ts. Luckily for us, the generate command from Nest’s CLI can be used to generate other files as well. Run:

nest generate service cat --no-spec

By specifying the --no-spec flag, you tell NestJS that you don’t want to generate a test file for the service this time. This will generate the following empty boilerplate for you, inside the same cat folder:

Copied to clipboard!
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatService {}
cat.service.ts

Notice the naming of the files, and the structure of your project. Every module is separated into a different folder with its own set of controller/resolver and service.


Integrating GraphQL

Now we have our own module, with its own service, but we also need a resolver for handling incoming requests. Of course, to work with GraphQL first, we need to install some dependencies. Namely:

npm i @nestjs/graphql graphql-tools graphql apollo-server-express

This will install four different packages, all needed to make GraphQL work properly with NestJS:

After everything is installed, let’s create that missing resolver. Again, we can use Nest CLI:

nest generate resolver cat --no-spec

To create the simplest query possible, change your cat.resolver.ts in the following way:

Copied to clipboard!
import { Resolver, Query } from '@nestjs/graphql';

@Resolver()
export class CatResolver {
    @Query(returns => String)
    async hello() {
        return 'đź‘‹';
    }
}
cat.resolver.ts

This will return a single waving emoji if you query for the hello field. Let’s not forget we also need to import the GraphQL module to our app.module.ts, as well as the CatModule that we’ve created earlier:

Copied to clipboard!
  import { Module } from '@nestjs/common';
  import { AppController } from './app.controller';
  import { AppService } from './app.service';
+ import { CatModule } from './cat/cat.module';
+ import { GraphQLModule } from '@nestjs/graphql';

@Module({
+     imports: [
+         CatModule,
+         GraphQLModule.forRoot({
+             autoSchemaFile: 'schema.gql'
+         })
+     ],
      controllers: [AppController],
      providers: [AppService],
  })
  export class AppModule {}
app.module.ts

This will get GraphQL working for us. The GraphQL module that we’ve specified as an import can take an object as an argument. Using the code first approach, we can specify an autoSchemaFile property, and pass a string as a value, that specifies the path where we want the schema to be generated for us by the GraphQL module. If you run your app, you will notice a schema.gql appearing at the root of your directory generated by Nest.

The code first approach uses TypeScript classes and decorators to generate a GraphQL schema, while using the schema first approach, you specify your GraphQL schema with SDL schema definition files.

If you are unfamiliar with how GraphQL works, I recommend checking out the article below to get you up and running.

How to Get Started With GraphQL

If you open up your localhost at localhost:3000/graphql and query for the hello field, you should get back a waving emoji đź‘‹

Running our first GraphQL query in NestJS
Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

Integrating MongoDB

The last step in integrating dependencies is to also integrate MongoDB, so we can persist our data. For this to work, we are going to need to install two more dependencies:

npm i --save @nestjs/mongoose mongoose

As you can see, we are going to use the popular Mongoose package. Once installed, you want to first connect your main module to your MongoDB by adding the MongooseModule to your imports:

Copied to clipboard!
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatModule } from './cat/cat.module';
import { GraphQLModule } from '@nestjs/graphql';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
    imports: [
        CatModule,
        GraphQLModule.forRoot({
            autoSchemaFile: 'schema.gql'
        }),
        MongooseModule.forRoot('mongodb://localhost/nest')
    ],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}
app.module.ts

I’m using a database called nest. Here you can specify an env variable to use a dev server during local development, and a production server when the app is deployed.

Defining a schema

Now to make us able to communicate with the database, we need to define a schema, as in Mongoose, everything starts with a schema. It defines a model that maps to a MongoDB collection. Once more, we can use decorators to create our schema. Put the following schema file inside your cat folder:

Copied to clipboard!
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

class Characteristics {
    lifespan: string
    size: 'small' | 'medium' | 'large'
    coat: 'short' | 'medium' | 'long'
    color: string
}

@Schema()
export class Cat {
    @Prop()
    breed: string;
    
    @Prop()
    characteristics: Characteristics;
}

export type CatDocument = Cat & Document;
export const CatSchema = SchemaFactory.createForClass(Cat);
cat.schema.ts

Here we export a new CatSchema that is created from the Cat class, and a CatDocument as well to indicate the type of the document. We will have a breed for each cat, and a couple of characteristics associated with them. Alternatively, if you don’t want to use decorators you can define your schema in a simpler way:

Copied to clipboard!
export const CatSchema = new mongoose.Schema({
    breed: String,
    characteristics: Characteristics
});
cat.schema.ts

To use this new schema inside our cat module, we need to load it through the imports array:

Copied to clipboard!
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatService } from './cat.service';
import { CatResolver } from './cat.resolver';
import { Cat, CatSchema } from './cat.schema';
@Module({
    imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
    providers: [CatService, CatResolver]
})
export class CatModule {}
cat.module.ts

Here we are using the forFeature method of the MongooseModule to configure the schema for the module. Now that we’ve registered the schema, we will need to inject the Cat model into our service, and then use the service inside our resolver to fetch and modify data in our database.

To inject the newly created CatModel into our service, modify your cat.service.ts accordingly:

Copied to clipboard!
import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './cat.schema';

@Injectable()
export class CatService {
    constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}

    async findAll(): Promise<Cat[]> {
        return this.catModel.find().exec();
    }
}
cat.service.ts

To inject the model into a service, you can use the @InjectModel decorator. Now we can use this.catModel to execute mongoose queries. I’ve already created an async function that will query all cats using the find method. The return type of this function will be a Promise of cats.


Extending the Resolver

So to actually use this findAll method, let’s go into our cat resolver and create a cats field for GraphQL:

Copied to clipboard!
import { Resolver, Query } from '@nestjs/graphql';
import { CatService } from './cat.service';
import { CatType } from './cat.dto';

@Resolver()
export class CatResolver {
    constructor(private readonly catService: CatService) {}

    @Query(returns => [CatType])
    async cats() {
        return this.catService.findAll();
    }
}
cat.resolver.ts

Using the constructor you can make the service available for the resolver. All we have to do here is define the return type and call the findAll method of the service. You’ll notice that the return CatType is coming from a file called cat.dto. This file will hold the data transfer object that defines how data will be sent over to our API. Create this file and add the following:

Copied to clipboard!
import { ObjectType, Field, InputType } from "@nestjs/graphql";

@ObjectType('Characteristics')
@InputType('CharacteristicsInput')
class Characteristics {
    @Field()
    lifespan: string

    @Field()
    size: 'small' | 'medium' | 'large'

    @Field()
    coat: 'short' | 'medium' | 'long'

    @Field()
    color: string
}

@ObjectType('CatType')
@InputType('CatInputType')
export class CatType {
    @Field()
    breed: string;

    @Field()
    characteristics: Characteristics;
}
cat.dto.ts

Here we have a couple of new decorators. @ObjectType and @InputType defines what type of data GraphQL will return, and what type of data it will accept when we want to update our cats collection. Since we want the same type for both queries and inputs, we need to use both decorators.

Why we are using classes here instead of interfaces? Classes are part of ES6 which means they will be preserved in the compiled JavaScript, unlike TypeScript interfaces. This is useful for Nest as it can refer to them during runtime. That is why Nest prefers using classes instead of interfaces.

Let’s go back into the GraphQL playground and execute a query. It will return an empty cats array as we haven’t added anything yet to our collection, but at least it returns an empty array which means that everything is working. So to actually create cats, let’s add a new method to our service and a new mutation for our resolver.

Querying MongoDB with GraphQL in NestJS

Creating cats

Starting from the service, add the following create method that we will call from the resolver:

Copied to clipboard!
async create(createCatDto: CatType): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
}
cat.service.ts

When we call this method, it will create a new document in our collection using CatType as a data transfer object. Based on this, we can add a new method to the resolver that calls this function:

Copied to clipboard!
@Mutation(returns => CatType)
    async createCat(@Args('input') input: CatType) {
        return this.catService.create(input);
    }
cat.resolver.ts

This time, we want to use the @Mutation decorator to let Nest know we want this method to be a mutation. And to pass arguments to it, we need to use the @Args decorator. GraphQL will expect this to be a CatType as well. Let’s go back to the playground and try to create a new cat in our database using this mutation.

Creating MongoDB collection with GraphQL in NestJS

Updating cats

There are two more things left to do to call our CRUD API complete: updating and removing cats. Let’s go with the first one. We already have most of the code for it, we just need to do some copy-pasting and a little update here and there:

Copied to clipboard!
async update(id: string, updateCatDto: CatType): Promise<Cat> {
    return this.catModel.findByIdAndUpdate(id, updateCatDto);
}
cat.service.ts

We need one more method for the service. This time, we will need to pass it an id as an argument to identify which document we want to update. Using the findByIdAndUpdate method of Mongoose, we can pass this id as well as the updated data to update our cats.

Copied to clipboard!
@Mutation(returns => CatType)
async updateCat(@Args('id') id: string, @Args('input') input: CatType) {
    return this.catService.update(id, input);
}
cat.resolver.ts

And of course, we need one more method for the resolver, which calls this service method. The only difference is that we need two arguments this time. Let’s try out this one in our playground:

Updating MongoDB collection with GraphQL in NestJS
Note that you’ll have to pass an ID to the mutation as well. To make fields optional, you want to use a PartialType

Removing cats

All that is left to do is to get rid of breeds that we deem unnecessary. You know how it goes by now; we need a new method for our service:

Copied to clipboard!
async delete(id: string): Promise<Cat> {
    return this.catModel.findByIdAndDelete(id);
}
cat.service.ts

and a new method for the resolver from which we can call this service:

Copied to clipboard!
@Mutation(returns => CatType)
async deleteCat(@Args('id') id: string) {
    return this.catService.delete(id);
}
cat.resolver.ts

And let’s give this one a try as well. Whatever you will query for, that field of the removed document will be returned.

RemovingMongoDB collection with GraphQL in NestJS

Summary

Now you have a fully working CRUD API in NestJS + GraphQL connected to MongoDB. For simple applications, this may seem like unnecessarily too much code, and it is. But as your application grows, your modules will be kept simple. Everything is in a new class, and well separated from the rest of the others. This also makes testing your NestJS application much easier.

If you would like to get the code in one piece, you can clone it from GitHub. Have you worked with Nest before? What are your impressions? Let us know in the comments below! Thank you for reading through, happy coding!

How to Get Started With TypeScript
  • twitter
  • facebook
Did you find this page helpful?
đź“š More Webtips
Mentoring

Rocket Launch Your Career

Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies:

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.