Skip to content

How to Write Non-Blocking APIs in NestJS ?

non-blocking APIs in nestJS

In this article, you gonna learn in detail about how to write non-blocking APIs in Nestjs. Which also includes background knowledge about what they are and also some coding examples to help you understand better.

In the realm of web development, building efficient and responsive APIs is a crucial skill. NestJS, a progressive Node.js framework, allows us to create non-blocking APIs that can handle a high volume of concurrent requests without causing delays. In this article, we’ll explore the principles and techniques behind writing non-blocking APIs in NestJS.

Table of Contents

  1. Introduction
    • What is a Non-blocking API?
    • Why is Non-blocking Important?
  2. Getting Started with Nest.js
    • Setting Up Your Nest.js Project
    • Creating Your First Controller
  3. Understanding Asynchronous Programming
    • Callbacks and Promises
    • The Power of Async/Await
  4. Implementing Non-blocking Operations
    • Leveraging Nest JS Middleware
    • Using Asynchronous Database Queries
  5. Optimizing Performance
    • Caching Techniques
    • Load Balancing for Scalability
  6. Error Handling in Non-blocking APIs in NestJs
    • Proper Error Handling Strategies
    • Handling Unhandled Rejections
  7. Debugging Techniques for Non-blocking APIs
  8. Conclusion
    • The Advantages of Non-blocking APIs
    • Next Steps for Your Nest JS Project

Introduction

In the world of web development, building high-performance APIs is crucial for delivering responsive and scalable applications. Nest.js, a progressive Node.js framework, is an excellent choice for creating APIs. In this article, we will explore how to write non-blocking APIs in Nest.js, ensuring that your applications can handle multiple requests efficiently without blocking or slowing down.

What is a Non-blocking API?

A non-blocking API is one that can continue processing requests without waiting for previous ones to complete. This allows for high concurrency and responsiveness in applications. In a non-blocking API, when a resource is requested, the server can immediately move on to handle the next request instead of waiting for the current one to finish.

Why is Non-blocking Important?

Non-blocking APIs are essential for handling a large number of users or requests concurrently. They prevent your application from becoming sluggish or unresponsive under heavy load. This is particularly important for real-time applications, such as chat systems, online gaming, and streaming services.

Getting Started with Nestjs

Setting Up Your NestJS Project

Let’s begin by setting up a basic Nest.js project. Make sure you have Node.js and npm (Node Package Manager) installed. If not, you can download and install them from nodejs.org.

First, install the Nest CLI globally:

  1. Install Nest CLI: Install the Nest CLI globally by running the following command:
npm install -g @nestjs/cli
  1. Create a Nest.js Project: Create a new Nest.js project using the following command:
nest new my-nest-project
  1. Navigate to Your Project: Change your working directory to the newly created project folder:
cd my-nest-project

Now that your Nest.js project is set up, let’s explore asynchronous programming in Nest.js.

If You want to use the react native with it, here is the guide for creating react native project: click here

Creating a Controller

In Nest.js, controllers handle incoming HTTP requests and route them to the appropriate service. Let’s create a basic controller for our non-blocking API.

1. Generate a Controller: Use the Nest CLI to generate a new controller. Replace “items” with your preferred controller name:

nest generate controller items

2. Navigate to the Controller: Open the items.controller.ts file created by the CLI. This file defines your controller class and its routes.

3. Define Routes: In your controller class, define the routes using decorators like @Get(), @Post(), @Put(), and @Delete(). For example:

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

@Controller('items')
export class ItemsController {
  @Get()
  findAll(): string {
    return 'This action returns all items';
  }
}

4. Run Your Application: With your controller in place, save the file and return to your terminal. Ensure your Nest JS application is running (you should see “Listening at” in your console). If not, start it with:

npm run start:dev

5. Access Your Controller: Open your web browser or a tool like Postman and access the controller route. In this case, open a browser and navigate to http://localhost:3000/items. You should see the response from your controller.

Congratulations! You’ve successfully created your first controller in Nest JS. This is the first step in building a non-blocking API. In the next sections, we’ll delve into the asynchronous nature of APIs and how to make them non-blocking.

Understanding Asynchronous Programming

Asynchronous programming is a concept in software development that’s all about managing tasks that take some time to finish. Imagine you’re cooking in the kitchen, and you have multiple dishes to prepare. Instead of waiting for one dish to be ready before starting the next, you can multitask and chop vegetables while the soup simmers. This is somewhat similar to how asynchronous programming works in the digital world.

Nest.js provides methods for handling asynchronous operations, including callbacks and Promises, and Async/Await. Understanding these concepts is crucial for writing non-blocking APIs.

Callbacks and Promises

Asynchronous programming is at the heart of building non-blocking APIs. Two common approaches for handling asynchronous operations in JavaScript are callbacks and promises.

Callbacks are functions that are passed as arguments to other functions and are executed once a specific task is completed. They are a basic building block of asynchronous code but can lead to callback hell when dealing with complex logic.

function fetchData(callback) {
  setTimeout(() => {
    const data = { message: 'Data fetched successfully' };
    callback(data);
  }, 1000);
}

fetchData((data) => {
  console.log(data.message);
});

Promises provide a cleaner way to manage asynchronous operations. They represent a value that might not be available yet but will be in the future. Promises can be in one of three states: pending, fulfilled, or rejected.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { message: 'Data fetched successfully' };
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data.message);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

Breaking Down The Code step by step:

  1. fetchData Function:
    • fetchData is a function that returns a Promise.Inside the Promise constructor, there is a setTimeout function. This function simulates a delay of 1000 milliseconds (1 second).
    After the 1-second delay, it resolves the Promise with an object containing a message.
  2. Promise Execution:
    • fetchData() is called, which returns the Promise.
    • The .then() method is chained to the Promise, indicating what should happen when the Promise is resolved successfully.
    • The .catch() method is also chained, specifying what to do in case of an error (if the Promise is rejected).
  3. .then() Block:
    • When the Promise inside fetchData resolves successfully (after 1 second), the code inside the .then() block is executed.Inside the .then() block, it accesses the message property of the resolved data object and logs it to the console.In this case, it logs “Data fetched successfully” to the console.
  4. .catch() Block:
    • The .catch() block specifies what to do if the Promise is rejected (if there’s an error).In this code, it logs an error message to the console, including the error object.
    Since there are no errors in the fetchData Promise, the .catch() block will not be executed in this specific code.

In summary, this code defines a function fetchData that returns a Promise with a simulated 1-second delay. When the Promise is resolved, it logs a success message to the console using .then(). If the Promise were to be rejected (indicating an error), it would log an error message using .catch(). However, in this code, there are no errors, so only the success message is logged to the console after the 1-second delay.

The Power of Async/Await

Async/Await is a more modern and concise way to work with Promises. While promises make asynchronous code more manageable, Nest JS takes it a step further with the async/await syntax. This allows you to write asynchronous code in a more synchronous style, making it easier to read and maintain.

async function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { message: 'Data fetched successfully' };
      resolve(data);
    }, 1000);
  });
}

async function main() {
  try {
    const data = await fetchData();
    console.log(data.message);
  } catch (error) {
    console.error('Error:', error);
  }
}

main();

Breaking down the code step by step:

  1. fetchData Function:
    • Inside fetchData, there is a Promise constructor. This Promise simulates a delay of 1000 milliseconds (1 second) using setTimeout. After the 1-sec delay, it resolves the Promise with an object containing a message.
  2. main Function: main is another async function that serves as the entry point of your program.
  3. try Block:
    • Within the try block, const data = await fetchData(); is called. This line awaits the result of the fetchData function. When fetchData completes (after 1-sec delay), it returns the resolved data.
  4. await Keyword:
    • The await keyword is used to pause the execution of the main function until the fetchData Promise is resolved.
  5. Logging the Data:
    • After the fetchData Promise is resolved, the code logs the data.message to the consoleIn this case, it logs “Data fetched successfully” to the console.
  6. catch Block:
    • If an error were to occur in the fetchData function (for example, if the Promise were to be rejected), the code would enter the catch block.
  7. Finally, the main function is called at the end of the code to start the execution of the program.

In summary, it defines an asynchronous function fetchData that simulates a delay and resolves a Promise with data. The main function uses await to wait for fetchData to complete and then logs the result. It also includes error handling using a try...catch block. When you run this code, it will wait for 1 second, fetch the data, and log “Data fetched successfully” to the console.

Building a Non-blocking API in Nest.js:

Leveraging Nest JS Middleware

Middleware functions in Nest JS provide a way to process incoming requests before they reach the route handler. They are an excellent place to introduce non-blocking operations, such as request logging, authentication, and rate limiting.

Here’s an example of a simple middleware that logs incoming requests:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`Request received: ${req.method} ${req.url}`);
    next();
  }
}

To use this middleware, add it to your controller:

import { Controller, Get, UseMiddleware } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';

@Controller('items')
@UseMiddleware(LoggerMiddleware)
export class ItemsController {
  // Your route handlers here
}

Following are the real world examples of non-blocking APIs in nest.js:

Example 1: Calling External API in Nestjs

Imagine you need to fetch data from an external API and return it as a response from your Nest.js API. For Example, to fetch data from the Google Weather API, you need to replace the URL in the axios.get call with the Google Weather API endpoint. Here’s how you can do it without blocking your application

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

@Controller('data')
export class DataController {
  @Get()
  async fetchData() {
    try {
      const response = await axios.get('https://weather.googleapi.com/data'); // Replace with the actual Google Weather API URL
      return response.data;
    } catch (error) {
      throw new Error('Failed to fetch data');
    }
  }
}

In this example, we use axios to make an asynchronous HTTP request to the external API. The async/await syntax ensures that our API remains non-blocking.

Example 2: Processing Concurrent Requests in Nestjs

Suppose you have an API endpoint that can handle multiple concurrent requests without blocking. You can achieve this using asynchronous programming:

Scenario: E-commerce Website Inventory Check

Imagine you’re building an e-commerce website, and you want to implement a feature that allows users to view the availability of multiple products simultaneously. Each product’s availability is determined by checking its inventory in a database. This is where you need concurrent requests.

Optimizing Performance of NestJS APIs:

Caching Techniques

Caching is a powerful strategy for improving the performance of your non-blocking API. By storing frequently used data in memory, you can reduce the need for time-consuming database queries or expensive calculations.

In Nest JS, you can implement caching using libraries like cache-manager or by utilizing built-in caching mechanisms for HTTP responses.

import { CacheInterceptor, Controller, Get, UseInterceptors } from '@nestjs/common';

@Controller('items')
@UseInterceptors(CacheInterceptor)
export class ItemsController {
  @Get()
  findAll() {
    // Your code to fetch items from the database
  }
}

Load Balancing for Scalability

As your application grows, it’s essential to ensure that it can handle increased traffic. Load balancing is a technique that distributes incoming requests across multiple server instances, ensuring that no single server is overwhelmed.

Nest JS can be easily deployed in a load-balanced environment, whether you’re using containerization with Docker or a cloud platform like AWS or Azure.

Error Handling in Non-blocking APIs in NestJS

Proper Error Handling Strategies

Robust error handling is crucial for maintaining the reliability of your non-blocking API in NestJS. Nest JS provides a variety of tools and techniques for managing errors, including custom exception filters and global exception handling.

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

Handling Unhandled Rejections

Unhandled promise rejections can crash your Node.js application. To prevent this, always ensure that you catch and handle errors in your async functions. You can use global error handlers or tools like process.on('unhandledRejection') to capture unhandled promise rejections.

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // You can log the error or perform other cleanup tasks here
});

Debugging Techniques for Non-blocking APIs in NestJS

Debugging non-blocking APIs can sometimes be challenging due to the asynchronous nature of the code. However, Nestjs provides several tools and techniques to help you pinpoint and resolve issues efficiently.

1. Logging:

Logging is a fundamental debugging technique. You can use libraries like winston or the built-in console module to log relevant information about the execution flow and variable values. Make sure to log both success and error cases to understand what’s happening.

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

@Controller('items')
export class ItemsController {
  private readonly logger = new Logger(ItemsController.name);

  @Get()
  async findAll() {
    try {
      const items = await this.itemsService.findAll();
      this.logger.log('Items retrieved successfully');
      return items;
    } catch (error) {
      this.logger.error('Error fetching items', error);
      throw error;
    }
  }
}

2. Error Handling:

Proper error handling is not only essential for production but also for debugging. Ensure that you catch and handle errors appropriately in your code. Use try-catch blocks to isolate problematic areas and log errors for investigation.

3. Unit Tests:

Unit tests are not only for verifying correctness but also for identifying issues. When a test fails, it indicates a problem in your code. Write comprehensive unit tests, and when a test fails, use it as a starting point for debugging.

4. Third-party Debugging Tools:

Consider using third-party debugging tools and profilers like New Relic, Node.js Inspector, or Visual Studio Code’s built-in debugging tools. These tools can provide insights into performance bottlenecks and issues that are hard to detect manually.


Conclusion

In this extensive guide, we’ve learned how to write non-blocking APIs in NestJS. We covered the basics, set up a project, created controllers and services, and made our code non-blocking using asynchronous programming techniques.

We then walked through the process of setting up a Nestjs project, creating controllers and services, and making service methods asynchronous. Additionally, we discussed optional but valuable practices like making middleware asynchronous, handling errors gracefully, and testing non-blocking APIs.

By following these practices, you can ensure that your Nest.js application remains responsive, efficient, and capable of handling multiple requests concurrently. Non-blocking APIs are a crucial part of building scalable and high-performance web applications.

Additional Resources

Leave a Reply

Your email address will not be published. Required fields are marked *