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
- Introduction
- What is a Non-blocking API?
- Why is Non-blocking Important?
- Getting Started with Nest.js
- Setting Up Your Nest.js Project
- Creating Your First Controller
- Understanding Asynchronous Programming
- Callbacks and Promises
- The Power of Async/Await
- Implementing Non-blocking Operations
- Leveraging Nest JS Middleware
- Using Asynchronous Database Queries
- Optimizing Performance
- Caching Techniques
- Load Balancing for Scalability
- Error Handling in Non-blocking APIs in NestJs
- Proper Error Handling Strategies
- Handling Unhandled Rejections
- Debugging Techniques for Non-blocking APIs
- 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:
- Install Nest CLI: Install the Nest CLI globally by running the following command:
npm install -g @nestjs/cli
- Create a Nest.js Project: Create a new Nest.js project using the following command:
nest new my-nest-project
- 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.
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:
fetchData
Function:fetchData
is a function that returns a Promise.Inside the Promise constructor, there is asetTimeout
function. This function simulates a delay of 1000 milliseconds (1 second).
- 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).
.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 themessage
property of the resolved data object and logs it to the console.In this case, it logs “Data fetched successfully” to the console.
- When the Promise inside
.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.
fetchData
Promise, the.catch()
block will not be executed in this specific code.- The
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:
fetchData
Function:- Inside
fetchData
, there is a Promise constructor. This Promise simulates a delay of 1000 milliseconds (1 second) usingsetTimeout
. After the 1-sec delay, it resolves the Promise with an object containing a message.
- Inside
main
Function:main
is anotherasync
function that serves as the entry point of your program.try
Block:- Within the
try
block,const data = await fetchData();
is called. This line awaits the result of thefetchData
function. WhenfetchData
completes (after 1-sec delay), it returns the resolved data.
- Within the
await
Keyword:- The
await
keyword is used to pause the execution of themain
function until thefetchData
Promise is resolved.
- The
- Logging the Data:
- After the
fetchData
Promise is resolved, the code logs thedata.message
to the consoleIn this case, it logs “Data fetched successfully” to the console.
- After the
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 thecatch
block.
- If an error were to occur in the
- 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.