Microservices in NestJS: How to build it? A Guide

Posted by Rashi Jaitly · 17 May, 2022 · 9 Min read
Microservices in NestJS: How to build it? A Guide

It's not easy to develop, test and deploy scalable applications. It's best to follow a software development lifecycle (SDLC) and follow best practices. Looking at the capabilities of an application, a microservice-based architecture has gained popularity in recent years.

NestJS is a Node.JS framework based on ExpressJS, but it's more than that. In fact, NestJS gives you everything you need to create microservices easily.

Also, read: NestJS vs ExpressJS: Which Framework To Choose?​​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​

This article will discuss microservice, how to create one and show how easy it is to use NestJS. There's no need for frameworks or libraries other than what NestJS provides by default so that anyone can benefit from it.

What Is A Microservice?

Microservice is a software architecture pattern where a large, complex application is broken down into many small, independent processes. These individual services are built for maximum performance, scalability, and maintainability.

They're also easier to develop in an Agile fashion because teams can work on individual services in isolation and then deploy them independently.

5 Advantages Of Building A Microservice

Some of the advantages of building a microservice are:

  1. Decoupled components - You can easily change or update a module without affecting other modules.
  2. Scalability - Microservices don’t share the same memory space, which makes it easy to scale your application and increase resources for a particular service when needed.
  3. Faster to build - Since you are splitting up your application into smaller pieces, you can start working on one module while still building other modules in parallel.
  4. Language and technology agnostic - You can use different frameworks and programming languages for each service if needed. This also lowers the barrier to entry since each team only needs to be proficient in one language or framework instead of having knowledge about all languages and frameworks used in the system.
  5. Reduces complexity - Microservices break down the application into smaller pieces that are easier to understand, maintain and test than a single monolithic codebase that does everything at once.
5 Advantages Of Building A Microservice

Why NestJS

NestJS is an open-source library for creating microservices with Node.js. It was created by the creators of the Express web application framework and further refined by their development team.

As you can imagine, NestJS is a perfect fit for building microservices, which are generally just a collection of small services that interact with each other to provide business functionality.

For example, NestJS can be used to quickly build a single app that provides all the functionality of several apps—allowing you to focus on your core business and not on writing boilerplate code for every little service you may need in your system.

Creating a Microservice with NestJS

Nest is an unopinionated, general-purpose framework for building backends. It can be used to build a monolithic app, a microservice app or a hybrid app. Nest uses TypeScript (a typed superset of JavaScript that compiles to plain JavaScript), which is quite easy to pick up if the developers have prior experience in languages such as Java or C#.

Also read: Monolith vs Microservices Architecture

Nest helps the developers use whatever libraries and tools you want within it, unlike other frameworks such as Meteor, where certain technologies are restricted from being used on the client side. This gives developers the freedom to pick and choose the right tools for your project, whether it’s Angular, React or Vue on the client side and MongoDB, MySQL or Postgres on the database side.

Creating your first application using Nest CLI

To start, download the NestJS CLI. The CLI helps bootstrap your new microservice so that it gets up and running quickly without having to first manually create a project folder or write out configurations from scratch.

$ npm i -g @nestjs/cli$ nest new nestjs-microservice

After you have created the necessary databases and the application is initialized, you’re going to use NestJS library to modify the boilerplate application and follow their protocol for creating the microservices using NestJS:

$ npm i --save @nestjs/microservices

Once installed, you can go ahead and replace the contents of your src/main.ts file with the code mentioned below:

import { NestFactory } from '@nestjs/core';

import { Transport } from '@nestjs/microservices';

import { AppModule } from 'src/app.module';

async function bootstrap() {

const port = process.env.PORT ? Number(process.env.PORT) : 8080;

const app = await NestFactory.createMicroservice(AppModule, {

transport: Transport.TCP,

options: {

host: '0.0.0.0',

port,

},

});

await app.listen(() => console.log('Microservice listening on port:', port));

}

bootstrap();

Developers having a general understanding of NestJS can easily read through this file. The only unique part is the way they are initializing the application server. Instead of using the default NestFactory.create() method, developers use NestFactory.createMicroservice() which gives them more explicit control over what endpoints the application responds to and how it negotiates HTTP/HTTPS connections with its users:

const app = await NestFactory.createMicroservice(AppModule, {

transport: Transport.TCP,

options: {

host: '0.0.0.0',

port,

},

});

In the above snippet, you’re declaring that you’re telling your microservice to listen to TCP requests on port (default is 8080). This means the service won't be a REST API, but will respond to a more raw request format.

Now let's take a look at the controller responsible for your routes and who responds to each method. First, you're going to want to open up src/app.controller.ts and remove all annotations since microservices respond with TCP requests instead of HTTP.

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

import { MessagePattern } from '@nestjs/microservices';

@Controller()

export class AppController {

@MessagePattern({ cmd: 'hello' })

hello(input?: string): string {

return `Hello, ${input || 'there'}!`;

}

}

As mentioned above, how you define and annotate methods in your NestJS controllers is different from how you defined them when using Nest's automatic code generation. Instead of using @Get(), @Post(), and other HTTP-specific annotations, you define your TCP interfaces using @MessagePattern() – an annotation that maps controller methods to incoming requests. You've defined the pattern to be any request that contains { cmd: 'hello'}.

hello(input?: string): string {

return `Hello, ${input || 'there'}!`;

}

Great! Now let’s make sure your microservice will start up. Your NestJS project comes pre-baked with a package.json file that includes all the appropriate commands for starting up your microservice locally, so let’s use that:

$ npm run start:dev

[5:41:22 PM] Starting compilation in watch mode...

[5:41:27 PM] Found 0 errors. Watching for file changes.

[Nest] 6361 - 04/31/2022, 5:41:28 PM [NestFactory] Starting Nest application...

[Nest] 6361 - 04/31/2022, 5:41:28 PM [InstanceLoader] AppModule dependencies initialized +20ms

[Nest] 6361 - 04/31/2022, 5:41:28 PM [NestMicroservice] Nest microservice successfully started +8ms

Microservice listening on port: 8080


Now, you've confirmed your service boots as expected, and now let's look at creating a Dockerfile for the service. By creating this file, you'll be able to create a portable, scalable image of your service that others can easily run without encountering any associated issues in different environments. This will mean that you can run it yourselves within a stable virtual environment, give it to team members so they can test it more efficiently, and deploy it to production-grade environments easily.

You're going to base your Dockerfile and node-env on an open-source image of a file server. From there, you'll install npm modules and run npm run build, which will transpile your TypeScript and minify the resulting code to optimize it.

# Start with a Node.js base image that uses Node v13

FROM node:13

WORKDIR /usr/src/app

# Copy the package.json file to the container and install fresh node_modules

COPY package*.json tsconfig*.json ./

RUN npm install

# Copy the rest of the application source code to the container

COPY src/ src/

# Transpile typescript and bundle the project

RUN npm run build

# Remove the original src directory (our new compiled source is in the `dist` folder)

RUN rm -r src

# Assign `npm run start:prod` as the default command to run when booting the container

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

Creating A Client Service

Creating a microservice is great, but that's not enough. The best way to test your new service properly is to ensure it's compatible with other services that might act as partners or peers.

To do this, you must learn how to create your microservices by starting with a NestJS project template, just like you did for the first one.

$ nest new client

You're also going to install two additional NestJS libraries here. The first is the Config Library, a convenient way to parse and manage application variables. You'll be using this later. The second is the Microservices Library, which contains several helper methods that make it easier to access other NestJS microservices.

$ npm i --save @nestjs/config @nestjs/microservices

You now have all of your required libraries installed so you can use them to build a client service for accessing the microservice you created earlier. In src/app.module.ts add the following:

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

import { ConfigModule, ConfigService } from '@nestjs/config';

import { ClientProxyFactory, Transport } from '@nestjs/microservices';

import { AppController } from './app.controller';

@Module({

imports: [ConfigModule.forRoot()],

controllers: [AppController],

providers: [

{

provide: 'HELLO_SERVICE',

inject: [ConfigService],

useFactory: (configService: ConfigService) =>

ClientProxyFactory.create({

transport: Transport.TCP,

options: {

host: configService.get('HELLO_SERVICE_HOST'),

port: configService.get('HELLO_SERVICE_PORT'),

},

}),

},

],

})

export class AppModule {}

The first thing that needs attention from the file contents above is the URL import. This allows ConfigService to be system-wide available for use in other areas of your application, like the application module:

imports: [ConfigModule.forRoot()];

Next, you’ll add the HELLO_SERVICE provider. This particular Service is where you can use Client Proxy Factory from the Nest Microservices Library to create a Service that works with other Services:

{

provide: 'HELLO_SERVICE',

inject: [ConfigService],

useFactory: (configService: ConfigService) => ClientProxyFactory.create({

transport: Transport.TCP,

options: {

host: configService.get('HELLO_SERVICE_HOST'),

port: configService.get('HELLO_SERVICE_PORT'),

},

}),

}

In the above excerpt, you’re registering a ClientProxy instance to the provider key HELLO_SERVICE listening on HELLO_SERVICE_PORT. These two values will load up appropriately from environment parameters. This type of parameterization is crucial when it comes to presenting an API that's compatible with many different environments (like dev, staging, and production) without modifications to your codebase and sensitive information like passwords or private keys needs to be kept out of source control.

Now you’ve created an instance of your proxy, open up src/app.controller.ts and set it up by pasting the following code into it:

import { Controller, Get, Inject, Param } from '@nestjs/common';

import { ClientProxy } from '@nestjs/microservices';

@Controller('hello')

export class AppController {

constructor(@Inject('HELLO_SERVICE') private client: ClientProxy) {}

@Get(':name')

getHelloByName(@Param('name') name = 'there') {

// Forwards the name to your hello service, and returns the results

return this.client.send({ cmd: 'hello' }, name);

}

}

Here you'll inject an instance of the client service into the controller. You’ve registered with Client for HELLO_SERVICE, so this is a key you use to designate which client class you want instances from:

constructor(

@Inject('HELLO_SERVICE') private client: ClientProxy

) {}

With a client who points to your TCP microservice, you can start sending custom requests that match the @MessagePattern you defined in the service:

@Get(':name')

getHelloByName(@Param('name') name = 'there') {

// Forwards the name to your hello service, and returns the results

return this.client.send({ cmd: 'hello' }, name);

}

The above line listens to a GET request on /hello/:name and forwards the request to your downstream TCP-based microservice. It then returns the results.

Just like you did with the downstream microservice, let's create a Dockerfile so the frontend service can be used to process payments. This new payment service should be able to communicate with other services in the system so you'll continue to use NestJS as its backend language. Here is what your new Dockerfile looks like:

# Start with a Node.js base image that uses Node v13

FROM node:13

WORKDIR /usr/src/app


# Copy the package.json file to the container and install fresh node_modules

COPY package*.json tsconfig*.json ./

RUN npm install


# Copy the rest of the application source code to the container

COPY src/ src/

# Transpile typescript and bundle the project

RUN npm run build

# Remove the original src directory (your new compiled source is in the `dist` folder)

RUN rm -r src

# Assign `npm run start:prod` as the default command to run when booting the container

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

Running Both Services Together

As you may have noticed, you haven’t yet tested your new client service (TCP Service). Though it uses the same npm run start:dev command as your TCP Service the two don’t overlap. Why? Because your new client service wraps all of that code up in Meteor code, including some environmental variables that aren’t set anywhere in any configuration file but instead need to be assigned as environment parameters for your application to boot up successfully.

This means that deploying your new client service requires a few extra steps beyond just running npm run start:dev.

Let’s take the time to understand all about microservices. For starters, you could use your service on an architectural level. Copy the following into architect.yml file at the root of your TCP service project directory:

# Meta data describing your component so others can discover and reference it

name: examples/nestjs-simple

description: Simple NestJS microservice that uses TCP for inter-process communication

keywords:

- nestjs

- examples

- tcp

- microservices

# List of microservices powering your component

services:

api:

# Specify where the source code is for the service

build:

context: ./

# Specify the port and protocol the service listens on

interfaces:

main:

port: 8080

protocol: tcp

# Mount your src directory to the container and use your dev command so you get hot-reloading

debug:

command: npm run start:dev

volumes:

src:

host_path: ./src/

mount_path: /usr/src/app/src/

# List of interfaces our component allows others to connect to

interfaces:

main:

description: Exposes the API to upstream traffic

url: ${{ services.api.interfaces.main.url }}


The above file does 3 things:

  • Describes the name, keyword, and component description for others to discover and refer to.
  • Outlines the services required by the component for operating.
  • Introduces interfaces that others can use from outside of component.

The component is wired up and ready to go! You just need to add the manifest file. For now, you can create it manually in this location:

# Install the Architect CLI

$ npm install -g @architect-io/cli

# Link the component to your local registry

$ architect link .

Successfully linked examples/nestjs-simple to local system at /Users/username/nestjs-microservice

# Deploy the component and expose the `main` interface on `http://app.localhost/`

$ architect dev examples/nestjs-simple:latest -i app:main

Using locally linked examples/nestjs-simple found at /Users/username/nestjs-microservice

http://app.localhost:80/ => examples--nestjs-simple--api--latest--qkmybvlf

http://localhost:50000/ => examples--nestjs-simple--api--latest--qkmybvlf

http://localhost:80/ => gateway

Wrote docker-compose file to: /var/folders/7q/hbx8m39d6sx_97r00bmwyd9w0000gn/T/architect-deployment-1598910884362.yml

[9:56:15 PM] Starting compilation in watch mode...

examples--nestjs-simple--api--latest--qkmybvlf_1 |

examples--nestjs-simple--api--latest--qkmybvlf_1 | [9:56:22 PM] Found 0 errors. Watching for file changes.

examples--nestjs-simple--api--latest--qkmybvlf_1 |

examples--nestjs-simple--api--latest--qkmybvlf_1 | [Nest] 32 - 04/31/2022, 9:56:23 PM [NestFactory] Starting Nest application...

examples--nestjs-simple--api--latest--qkmybvlf_1 | [Nest] 32 - 04/31/2022, 9:56:23 PM [InstanceLoader] AppModule dependencies initialized +29ms

examples--nestjs-simple--api--latest--qkmybvlf_1 | [Nest] 32 - 04/31/2022, 9:56:23 PM [NestMicroservice] Nest microservice successfully started +16ms

examples--nestjs-simple--api--latest--qkmybvlf_1 | Microservice listening on port: 8080

Before you know your TCP-based service could be deployed via Architect, let’s go ahead and create another component to represent your upstream REST API. Since this will be connecting to the previous one, you need it available to connect to via dependency. To do this, paste the following into another architect.yml file in the REST API project root directory:

# architect.yml

name: examples/nestjs-simple-client

description: Client used to test the connection to the simple NestJS microservice

keywords:

- nestjs

- examples

- microservice

- client

# Sets up the connection to your previous microservice

dependencies:

examples/nestjs-simple: latest


services:

client:

build:

context: ./

interfaces:

main: 3000

environment:

# Dyanmically enriches your environment variables with the location of the other microservice

HELLO_SERVICE_HOST: ${{ dependencies['examples/nestjs-simple'].interfaces.main.host }}

HELLO_SERVICE_PORT: ${{ dependencies['examples/nestjs-simple'].interfaces.main.port }}

debug:

command: npm run start:dev

volumes:

src:

host_path: ./src/

mount_path: /usr/src/app/src/


# Exposes your new REST API to upstream traffic

interfaces:

client:

description: Exposes the REST API to upstream traffic

url: ${{ services.client.interfaces.main.url }}


Just like you’ve done with the previous component, let's make sure you can deploy the new one.

# Link the component to your local registry

$ architect link .

Successfully linked examples/nestjs-simple-client to local system at /Users/username/nestjs-microservice-client


# Deploy the component and expose the `main` interface on `http://app.localhost/`

$ architect dev examples/nestjs-simple-client:latest -i app:client

Using locally linked examples/nestjs-simple-client found at /Users/username/nestjs-microservice-client

Using locally linked examples/nestjs-simple found at /Users/username/nestjs-microservice

http://app.localhost:80/ => examples--nestjs-simple-client--client--latest--qb0e6jlv

http://localhost:50000/ => examples--nestjs-simple-client--client--latest--qb0e6jlv

http://localhost:50001/ => examples--nestjs-simple--api--latest--qkmybvlf

http://localhost:80/ => gateway

Wrote docker-compose file to: /var/folders/7q/hbx8m39d6sx_97r00bmwyd9w0000gn/T/architect-deployment-1598987651541.yml

[7:15:45 PM] Starting compilation in watch mode...

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 |

examples--nestjs-simple--api--latest--qkmybvlf_1 | [7:15:54 PM] Found 0 errors. Watching for file changes.

examples--nestjs-simple--api--latest--qkmybvlf_1 |

examples--nestjs-simple--api--latest--qkmybvlf_1 | [Nest] 31 - 09/01/2020, 7:15:55 PM [NestFactory] Starting Nest application...

examples--nestjs-simple--api--latest--qkmybvlf_1 | [Nest] 31 - 09/01/2020, 7:15:55 PM [InstanceLoader] AppModule dependencies initialized +18ms

examples--nestjs-simple--api--latest--qkmybvlf_1 | [Nest] 31 - 09/01/2020, 7:15:55 PM [NestMicroservice] Nest microservice successfully started +9ms

examples--nestjs-simple--api--latest--qkmybvlf_1 | Microservice listening on port: 8080

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [7:15:55 PM] Found 0 errors. Watching for file changes.

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 |

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [NestFactory] Starting Nest application...

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [InstanceLoader] ConfigHostModule dependencies initialized +18ms

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [InstanceLoader] ConfigModule dependencies initialized +1ms

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [InstanceLoader] AppModule dependencies initialized +2ms

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [RoutesResolver] AppController {/hello}: +6ms

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [RouterExplorer] Mapped {/hello, GET} route +5ms

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [RouterExplorer] Mapped {/hello/:name, GET} route +2ms

examples--nestjs-simple-client--client--latest--qb0e6jlv_1 | [Nest] 30 - 09/01/2020, 7:15:56 PM [NestApplication] Nest application successfully started +3ms

As an example, deploying the TCP-service, your upstream HTTP service and enriching the networking so that both services are automatically talking to each other can be achieved by executing a command. Running the following command deploys examples/nestjs-simple-client locally and exposes the client interface at http://app.localhost/hello/world.

$ architect dev examples/nestjs-simple-client:latest -i app:client

Congratulations! That’s all it takes for a locally runnable component to be deployed to a remote cluster with Architect. Once that’s done, you will see the output from your application in the same output panel. There might be some latency from when the video screencast is being recorded and when the actual live application is deployed, but that should not take more than a few seconds.

Frequently Asked Questions

Subscribe to The Friday Brunch
Our twice a month newsletter featuring insights and tips on how to build better experiences for your customers
READ ALSO

Have a product idea?

Talk to our experts to see how you can turn it
into an engaging, sustainable digital product.

SCHEDULE A DISCOVERY MEETING