Upskill/Reskill
Nov 27, 2024

Configure Microservices in NestJS with MySQL and Postman: A Beginner’s Guide

Zziwa Raymond

Monolithic architecture was the predominant approach to backend development before 2011. In this model, the entire application is structured as a single, unified codebase, where all components and services are tightly coupled and deployed together as one module. The monolithic approach encapsulates all business logic, data access, the user interface (UI) and other functionalities within a single executable or application.

While the monolithic approach offers simplicity in development and deployment, it introduces significant challenges as applications scale. With a single codebase, even minor changes necessitate rebuilding and redeploying the entire application, resulting in longer development cycles and higher risk of introducing errors. Moreover, scaling a monolithic application is often inefficient, as it typically requires scaling the entire system, even if demand increases in only one component. The tight coupling of components also leads to interdependencies, making the system more fragile and harder to maintain as teams and codebases grow.

The most critical drawback of monolithic architecture, however, is its extensive failure impact, often described as the “burst radius.” A failure in one component can cause the entire system to go down, leading to significant downtime. This interconnection heightens the risk of widespread outages and complicates troubleshooting and recovery. A single issue can cascade through the entire system, making it difficult to isolate and resolve without affecting other parts. Consequently, organizations often face prolonged downtimes, which can have a severe impact on business operations and the overall user experience.

Despite these drawbacks, the monolithic method was the standard for many years due to its simplicity and the lack of alternatives. However, microservices and other new architectural paradigms have provided more flexible and scalable solutions.

What Are Microservices?

In a microservices architecture, an application is composed of small, independent services that communicate with each other through well-defined APIs. Microservices divide an application into distinct, loosely coupled services. Each service is responsible for a specific piece of functionality; for example, in an e-commerce backend application, user authentication, payment processing, inventory management and other services, can be developed, deployed and scaled independently. This provides numerous advantages, including:

  • Scalability: Microservices enable the independent scaling of individual services. When one service experiences high demand, it can be scaled without impacting the entire application. This optimizes resource utilization and enhances overall performance.
  • Technology flexibility: In a microservices architecture, each service can be developed using the most suitable technologies, languages or frameworks for its specific needs. This flexibility allows development teams to select the best tools for each task.
  • Fault isolation: Due to the independent nature of microservices, a failure in one service is less likely to affect the entire system. This minimizes the impact of failures, enhances system resilience and reduces downtime.
  • Development and deployment speed: Microservices allow teams to work on different services concurrently, accelerating development processes. Continuous integration and deployment practices are easier to implement, enabling faster updates and more frequent improvements.
  • Simplified maintenance and updates: The modular structure of microservices makes maintaining and updating applications more straightforward. Changes can be made to individual services without affecting others, reducing the risk of errors and simplifying the testing process.
  • Organizational alignment: Microservices facilitate organizing teams around specific business capabilities. Each team can take full ownership of a service, from development through deployment and support, leading to increased autonomy, accountability and efficiency.
  • Enhanced agility: The modular design of microservices supports iterative development, allowing for more responsive adaptation to changing business needs and fostering rapid innovation.

Monoliths vs. Microservices: Structural Differences

In a monolithic application, all client requests are handled by a single, general controller. This controller is responsible for processing the requests, executing the necessary commands or operations and returning the responses to the client. Essentially, all business logic and request handling is centralized, which simplifies the development process.

In contrast, a microservices architecture introduces additional complexity with the inclusion of an application gateway. The application gateway serves as a crucial intermediary layer in a microservices setup. Here’s how it works:

  1. Request handling: The gateway receives all incoming requests from clients.
  2. Routing: It then determines the appropriate microservice or controller that should handle each request based on its routing rules.
  3. Service interaction: The selected controller interacts with the corresponding microservice to process the request.
  4. Response aggregation: Once the microservice has completed its task, it sends the result back to the controller, which then forwards it to the gateway.
  5. Client response: Finally, the gateway returns the processed response to the client.

This layered approach separates the concerns of request routing and business logic, allowing each microservice to focus on its specific functionality while the gateway manages request distribution and response aggregation. If this sounds complex, don’t worry — I will walk through each component in detail and explain how it all works together.

Implementing Microservices With NestJS

NestJS is a progressive Node.js framework that leverages TypeScript, offering a powerful combination of modern JavaScript features, object-oriented programming and functional programming paradigms. It is designed to provide a native application architecture that helps developers build highly testable, scalable and maintainable applications.

In this tutorial, I will show you how to implement microservices using NestJS as the primary technology, NATS as the communication medium, Prisma as the object-relational mapping (ORM) technology, MySQL as the database and finally Postman to test endpoints.

This approach will demonstrate how to effectively manage microservices, ensuring they communicate seamlessly, are easily scalable and can be deployed reliably in a production environment. Along the way, I’ll cover best practices for setting up a microservices architecture, managing dependencies and securing a deployment, creating a solid foundation for building robust and efficient distributed systems.

Set Up the Base NestJS Application

Before you begin, ensure Node.js is Installed. Node.js is essential for running JavaScript code server-side and managing packages. If you haven’t installed Node.js yet, you can download it from the official Node.js website. Next, use npm (which comes bundled with Node.js) to install the Nest command-line interface (CLI), a tool that simplifies the creation and management of NestJS applications.

With the Nest CLI installed, set up your base NestJS application to serve as your gateway and name it api-gateway:

npm install -g @nestjs/cli //--this command is to install the nest cli
nest new api-gateway //--this command is to scaffold our base application..you can change `api-gateway` to any name you want

Launch your preferred text editor (e.g., VS Code, Sublime Text) and open the parent directory of the NestJS application (the directory that contains the parent folder of your base application). Navigate into the base application folder (i.e., api-gateway) and open a new terminal instance. Most modern text editors have built-in terminal capabilities. For VS Code, you can open the terminal by selecting Terminal from the top menu and then New Terminal. For Sublime Text, you might need to use a plug-in like Terminus to open a terminal within the editor.

cd api-gateway //-- to navigate into the parent folder of my base application
npm run start:dev //--to start the development server

Build the Backend Application

With your base application successfully up and running, the next step is to build a foundational backend application for a blog site. You’ll implement two separate services in this tutorial: one for managing readers, and another for handling create, read, update and delete (CRUD) operations for your blog articles. If you’ve worked with NestJS before, the project structure will be familiar and straightforward. However, I’ll provide a brief overview of the structure in case you’re not sure how it’s organized.

When you scaffold a new NestJS project, the default structure typically includes:

1. src: This is the main directory where most of your application code resides.

  • app.module.ts: The root module that ties together the different parts of the application.
  • app.controller.ts: The controller responsible for handling incoming requests and returning responses.
  • app.service.ts: The service that contains the business logic; can be injected into controllers.
  • main.ts: The entry point of the application, where the NestJS app is bootstrapped.

2. test: This directory contains the test files for your application.

  • app.e2e-spec.ts: The end-to-end test file.
  • jest-e2e.json: The configuration file for end-to-end testing with Jest.

3. node_modules: This directory holds all the installed dependencies for the project.

4. package.json: This file lists the dependencies and scripts for your project.

5. tsconfig.json: The TypeScript configuration file.

6. nest-cli.json: The configuration file for the NestJS CLI.

Create the Microservices and Gateway

The next step is to create two additional applications that will serve as microservices and name them reader-mgt and article-mgt. These applications will function as independent microservices within the architecture. After that, install the @nestjs/microservices and nats libraries to enable communication between services. Then configure these two applications to listen for requests via NATS, ensuring they can handle the incoming messages accordingly.

So, in the same folder, run the following commands:

nest new reader-mgt //--create reader management microservice
nest new article-mgt //-- create article management service

With the two services now scaffolded, configure your gateway to handle client requests and route them to the appropriate services. First, install the @nestjs/microservices and nats dependencies. Then create a NATS module, which will be registered in the API gateway‘s app module to enable proper communication between the gateway and the microservices:

npm install @nestjs/microservices nats //--to install the dependencies

If you’re not already in the gateway folder, use the cd command to navigate into it. Once there, go to the src folder and create a new directory named nats-client, which will serve as the location for your NATS client configuration. After that, create a file named nats.module.ts within the nats-client folder, and add the following code:

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';

@Module({
 imports: [
   ClientsModule.register([
     {
       name: 'NATS_SERVICE',
       transport: Transport.NATS,
       options: {
         servers: ['nats://localhost:4222'],
       },
     },
   ]),
 ],

 exports: [
   ClientsModule.register([
     {
       name: 'NATS_SERVICE',
       transport: Transport.NATS,
       options: {
         servers: ['nats://localhost:4222'],
       },
     },
   ]),
 ],
})
export class NatsClientModule {}

This code creates a NatsClientModule, which you’ll later register in the API gateway’s app module. First, it imports the Module decorator along with the ClientsModule and Transport declarations from the @nestjs/microservices library that you installed earlier. Next, it registers the NATS_SERVICE and specifies the transport as Transport.NATS. NestJS supports various transport clients by default, but for this example, stick with NATS.

Then it defines an options object, which specifies the servers property and sets the NATS server address to nats://localhost:4222. Finally, it exports the registered NATS clients to make them accessible to other modules, which is useful if you have multiple modules within the gateway. For instance, you might have a users module, articles module, readers module and more. However, this tutorial uses a single controller and module for readers and articles.

With this complete, you can now proceed to the app.module.ts file and register the NatsClientModule:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { NatsClientModule } from './nats-client/nats.module';

@Module({
 imports: [NatsClientModule],
 controllers: [AppController],
 providers: [],
})
export class AppModule {}

At this point, you’re about 80% done with configuring the API gateway. The last step is to define the API routes in the app.controller.ts file. Navigate to that file and add the following code:

import { Controller, Get, Inject, Post, Req } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Controller('api/')
export class AppController {
 constructor(
   @Inject('NATS_SERVICE') private readonly natsClient: ClientProxy,
 ) {}

 @Post('/save-reader')
 saveReader(@Req() req: Request) {
   return this.natsClient.send({ cmd: 'SAVE_READER' }, req.body);
 }

 @Get('get-all-readers')
 getReaders(@Req() req: Request) {
   return this.natsClient.send({ cmd: 'GET_ALL_READERS' }, req.body);
 }

 @Post('/save-article')
 saveArticle(@Req() req: Request) {
   return this.natsClient.send({ cmd: 'SAVE_ARTICLE' }, req.body);
 }

 @Get('/get-all-articles')
 getAllArticles(@Req() req: Request) {
   return this.natsClient.send({ cmd: 'GET_ALL_ARTICLES' }, req.body);
 }

 @Post('delete-article')
 deleteArticle(@Req() req: Request) {
   return this.natsClient.send({ cmd: 'DELETE_ARTICLE' }, req.body);
 }
}

This defines the API routes that will handle incoming HTTP requests and forward them to the NATS service for processing. The AppController class uses the @Controller decorator to specify the base route for all endpoints, which is 'api/'. Within this controller, inject the NATS client using the @Inject decorator, associating it with the 'NATS_SERVICE' token. The controller includes several endpoints: POST /save-reader and GET /get-all-readers for managing readers, and POST /save-article, GET /get-all-articles and POST /delete-article for managing articles. Each endpoint method uses the natsClient.send method to send commands to the NATS service, passing the request body as the payload. This setup allows the API gateway to relay client requests to the appropriate microservices via NATS.

Finally, execute the npm run start:dev command to start the API gateway application. This will verify that the application runs smoothly and without any errors.

Figure 1: The api-gateway application

Configure Communication Services

Next, configure your services to handle requests from the running API gateway, process them and send responses back. However, before proceeding, there’s an important step: setting up the NATS server locally. Since you specified the NATS server address as nats://localhost:4222, both the gateway and services will expect a NATS server running on your local machine.

For development purposes, install the NATS server locally. Although you can run this in a Docker container, work with a local setup for simplicity. For Linux and macOS users, install the NATS server using brew install nats-server and run nats-server to start the service. For Windows users, use choco install nats-server. If choco is not recognized, ensure Chocolatey is installed by running:

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

Then verify the installation by running choco --version. If you need further guidance, consult the NATS documentation.

Figure 2: NATS server running locally

Configure Your First Service

Now you can configure your first service, article-mgt. Go to the main.tsfile, which is the entry point of this service, and replace the default code with:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
 const app = await NestFactory.createMicroservice<MicroserviceOptions>(
   AppModule,
   {
     transport: Transport.NATS,
     options: {
       servers: ['nats://localhost:4222'],
     },
   },
 );
 await app.listen();
}
bootstrap();

This code transforms article-mgt from a standalone application into a NestJS microservice instance and configures it to use NATS as the transport mechanism, specifying the server address (nats://localhost:4222) to connect to the NATS server.

Next, create a new directory named dto within the src folder, and then create a file named dto.ts, which will house the expected payload structure. DTO stands for data transfer objects, which are simple objects used to transfer data between different layers of an application, especially during network requests. In this context, DTOs help define the structure and type of the payload that the backend application expects from client requests. You can implement further validation using the class-validatordependency, if needed. However, to keep this article focused, we won’t use it here. You can learn more about class-validator in the NestJS official documentation, if you’re interested.

export class saveArticleDto {
 title: string;
 content: string;
}

export class deleteArticleDto {
 id: string;
}

This code defines two DTOs for handling data in the article-mgt service. The saveArticleDto class specifies the structure for saving an article, requiring a title and content. And the deleteArticleDto class defines the structure for deleting an article, which requires an id to identify the article to be removed. These DTOs help ensure that the data passed between different parts of the application is well-defined, consistent and matches the expected type. There are three routes for articles but only two DTO classes are defined. This is because the third route, which retrieves all articles, does not require any payload.

Now go to the app.controller.ts file and modify the code.

Figure 3: Code in app.controller.ts

You might notice the red squiggly lines under the function names in the controller methods; this is because you haven’t yet defined these functions in app.service.ts. Before addressing that, let me explain the code: It imports the DTOs to enforce type checking on payloads, ensuring the data passed to the functions meets the expected structure. The @MessagePattern decorator specifies how messages should be handled. It takes an object where the cmd property defines a command string. This string must match the command specified earlier in the API gateway. The API gateway uses this command to determine which function to call for a given API request, attaching the command to the request before forwarding it.

Use Prisma To Interact With Your Database

To interact with your database using Prisma, create a Prisma module and service that you can use in the app.service.ts file. Start by creating a folder named prisma inside the src directory. Then, within this folder, create two files: prisma.module.ts and prisma.service.ts. The PrismaService in prisma.service.ts extends the PrismaClient class from Prisma and customizes the Prisma client by configuring the database connection URL using the DATABASE_URL environment variable. The PrismaModule in prisma.module.ts defines a module that provides the PrismaService, allowing it to be injected and used in other parts of the microservice for database operations.

//prisma.service.ts file
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { env } from 'process';

@Injectable()
export class PrismaService extends PrismaClient {
 constructor() {
   super({
     datasources: {
       db: {
         url: env.DATABASE_URL,
       },
     },
   });
 }
}
//prisma.module.ts file
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
 providers: [PrismaService],
})
export class PrismaModule {}

Now, move to the app.service.ts and add the code below to define the necessary functions. To explain briefly: The saveArticle function takes data as a parameter, which must be of the saveArticleDto type, as defined earlier. The function uses a try-catch block to handle the process. First, it attempts to insert the data into the database. After that, it calls the getAllArticles function to retrieve the updated list of articles. Since getAllArticles is an asynchronous function, it uses the await keyword. Once this is done, the function returns an object as a response, containing the HttpCode, which includes the statusCode, a message and a string. Additionally, the getAllArticles function returns all articles from the database, and the deleteArticle function handles the deletion of an article based on the provided ID.

//app.service.ts file
import { Injectable } from '@nestjs/common';
import { deleteArticleDto, saveArticleDto } from './dto/dto';
import { PrismaService } from './prisma/prisma.service';

@Injectable()
export class AppService {
 constructor(private readonly prismaService: PrismaService) {}

 async saveArticle(data: saveArticleDto) {
   try {
     await this.prismaService
       .$queryRaw`INSERT INTO article (title, content) VALUES (${data.title}, ${data.content})`;
     const articles = await this.getAllArticles();
     return {
       HttpCode: 201,
       message: 'Article saved successfully',
       data: articles.data,
     };
   } catch (error) {
     console.log(error);
     return {
       HttpCode: 400,
       message: 'Error saving article',
       data: null,
     };
   }
 }

 async getAllArticles() {
   try {
     const articles = await this.prismaService
       .$queryRaw`SELECT * FROM article`;

     return {
       HttpCode: 201,
       message: 'Article saved successfully',
       data: articles,
     };
   } catch (error) {
     console.log(error);
     return {
       HttpCode: 400,
       message: 'Error while fetching articles',
       data: null,
     };
   }
 }

 async deleteArticle(data: deleteArticleDto) {
   try {
     await this.prismaService
       .$queryRaw`DELETE FROM Article WHERE id = ${data.id}`;

     const articles = await this.getAllArticles();

     return {
       HttpCode: 200,
       message: 'Article deleted successfully',
       data: articles,
     };
   } catch (error) {
     console.log(error);
     return {
       HttpCode: 400,
       message: 'Error while deleting article',
       data: null,
     };
   }
 }
}

Start Your First Service

With that accomplished, you can now start your articles-mgt service and check if it runs smoothly without any errors. To do this, simply execute the command 'npm run start:dev'.

Figure 4: article-mgt service

Configure Your Second Service

Now that you’ve completed the article-mgt microservice configuration, move on to configuring the reader-mgt service. This service will handle two primary operations: registering readers and retrieving all registered readers. Since the setup process is quite similar to what I’ve already covered, I’ll skip the detailed explanations to save time. The implementation is essentially the same, just within a different service context.

To set up the reader-mgt service, start by navigating to the reader-mgtdirectory. Since the main.ts file in this service will have the same code as the article-mgt service, you can simply copy the content from the article-mgt main.ts file and paste it into the corresponding file in reader-mgt. Next, copy the entire prisma directory from the article-mgt service into the reader-mgt service. However, this won’t work immediately; you’ll need to install and initialize Prisma in the reader-mgt service. Run npm install Prisma @prisma/client to install Prisma, and then execute npx prisma generate to initialize it. Additionally, define the schema for the readers and perform a migration. Don’t forget to copy the database connection string from the .env file in article-mgt because without it, the reader-mgt microservice won’t be able to connect to the database.

Figure 5: Reader and article models

After defining the reader schema, run npx prisma migrate dev to apply the migration to the database, which will add the reader table to the MySQL database.

The final step involves configuring the app.controller.ts and app.service.ts files in the reader-mgt service. This process is similar to what you did in the article-mgt service. In the controller, define the routes, and then map these routes to corresponding functions in the service. You can use the article-mgt microservice configuration as a reference to guide you through this process.

//app.service.ts
import { Injectable } from '@nestjs/common';
import { saveReaderDto } from './dto/dto';
import { PrismaService } from './prisma/prisma.service';

@Injectable()
export class AppService {
 constructor(private readonly prismaService: PrismaService) {}

 async saveReader(data: saveReaderDto) {
   try {
     await this.prismaService
       .$queryRaw`INSERT INTO reader (email) VALUES (${data.email})`;
     const readers = await this.getAllReaders();
     return {
       HttpCode: 201,
       message: 'Reader saved successfully',
       data: readers.data,
     };
   } catch (error) {
     console.log(error);
     return {
       HttpCode: 400,
       message: 'Error saving reader',
       data: null,
     };
   }
 }

 async getAllReaders() {
   try {
     const readers = await this.prismaService.$queryRaw`SELECT * FROM reader`;

     return {
       HttpCode: 201,
       message: 'Readers fetched successfully',
       data: readers,
     };
   } catch (error) {
     console.log(error);
     return {
       HttpCode: 400,
       message: 'Error while fetching articles',
       data: null,
     };
   }
 }
}
//app.controller.ts
import { Controller } from '@nestjs/common';
import { AppService } from './app.service';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { saveReaderDto } from './dto/dto';

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

 @MessagePattern({ cmd: 'SAVE_READER' })
 async saveReader(@Payload() data: saveReaderDto) {
   return this.appService.saveReader(data);
 }

 @MessagePattern({ cmd: 'GET_ALL_READERS' })
 async getAllReaders() {
   return this.appService.getAllReaders();
 }
}
//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma/prisma.service';

@Module({
 imports: [],
 controllers: [AppController],
 providers: [AppService, PrismaService],
})
export class AppModule {}

Run Your Microservice

After configuring the app.service.ts, app.module.ts and app.controller.ts files for the reader-mgt service, the final step is to run the microservice to ensure that everything is functioning correctly and that there are no errors. This involves verifying that the routes in the controller correctly map to the functions in the service and that the microservice can handle requests as expected.

Once you’ve confirmed that all configurations are in place, you can start the reader-mgt service using the npm run start:dev command. This will launch the service in development mode, allowing you to check for any issues and ensure that the service is working seamlessly.

Figure 6: reader-mgt microservice

Test Your Application

If you’ve made it this far, congratulations! The coding portion of your project is complete, and your api-gateway, reader-mgt and article-mgt services are up and running without any errors. The next step is to use Postman to test the application and ensure that it performs as expected. Use Postman to send requests to the API gateway and verify that the operations are being correctly handled by the microservices. This will help confirm that all parts of the application are working together seamlessly.

Figure 7: /save-reader endpoint

Figure 8: /get-all-readers

The image illustrates the flow of the save-reader and get-all-readersendpoints as they pass through the API gateway and reach the reader-mgtmicroservice. The API gateway first receives the requests, identifies the correct commands and forwards them to the reader-mgt service via NATS. The reader-mgt service then processes the requests by creating a new reader or retrieving all readers. Once the requests are successful, the service returns the appropriate response.

Next, test the article-mgt endpoints by sending requests to create, delete and retrieve articles. First, send three create requests to the /save-articleendpoint to add three articles to the database, as shown in Figure 9. Then send a request to the /delete-article endpoint to delete the article with an ID of 2. Finally, make a GET request to the /get-all-articles endpoint to retrieve the updated list of articles, confirming that the deletion was successful and the remaining articles are correctly listed in the database.

Figure 9: /save-article requests

Figure 10: /delete-article request

Figure 11: /get-all-articles request

Conclusion

Congratulations on reaching the end of this comprehensive setup guide! You’ve navigated through the intricacies of configuring a robust microservices architecture using NestJS, Prisma, MySQL and NATS. While you’ve successfully set up a functional microservices architecture, there’s always room for further enhancements.

As you continue to develop your application, consider implementing additional features such as robust error handling, security measures and comprehensive logging. Exploring Docker for containerization and Kubernetes for orchestration could further streamline your development and deployment processes.

Thank you for following along with this guide. Your dedication to mastering these technologies will undoubtedly pave the way for creating sophisticated and resilient applications. If you need the code for this blog, find it in my GitHub repository. Happy coding, and best of luck with your continued development!

About the author: Zziwa Raymond Ian

Zziwa Raymond Ian is a full-stack engineer and a member of the Andela Talent Network, a private global marketplace for digital talent. Specializing in Next.js, React, JavaScript, TypeScript, NestJs and others, he has developed a deep holistic understanding of both frontend and backend technologies. Zziwa loves to tackle diverse and difficult technology challenges, as they are a driving force in his continuous learning.

Interested in 
Learning More?

Subscribe today to stay informed and get regular updates from Andela.

You might also be interested in

Ready to get started?

Contact Us