Views

Useful Tips To Building Nest.js APIs To handle Thousands of Request Per Second

Achieving high scalability in backend development is a crucial challenge, especially when dealing with large amounts of traffic. Nest.js can save this

Building a Nest.js API The right way

Want to build a Nest.js API that scales to millions of requests without breaking? Achieving high scalability in backend development is a crucial challenge, especially when dealing with large amounts of traffic. Nest.js is a powerful framework, but it requires proper optimization to handle heavy loads effectively. If your API is not prepared for sudden traffic spikes, it can slow down significantly or even crash, causing downtime and a poor user experience. In this guide, we will explore the key techniques that will help you optimize your Nest.js API for high performance and stability.

The Damaging Nature Of High Traffic On Your API

When your API starts receiving thousands or even millions of requests, several issues can arise. If the system isn’t designed to handle such a workload, it can lead to increased latency, server crashes, and poor overall performance. This is especially true if the main thread is blocked by synchronous operations, making it difficult to process incoming requests efficiently. Additionally, poorly optimized database queries can slow down response times, creating a bottleneck for data retrieval.

Memory leaks are another common issue in APIs under heavy traffic, as they gradually consume server resources until performance degrades. Without a proper rate-limiting mechanism, bots and malicious users can overwhelm your API with excessive requests, leading to server overload. These factors combined can severely impact the reliability of your API. However, by implementing best practices and strategic optimizations, you can ensure that your Nest.js API remains stable even under extreme load.

1. Use Asynchronous & Non-Blocking Code

One of the fundamental principles of building a high-performance API is to use asynchronous, non-blocking code. When the main thread is blocked by time-consuming operations, it prevents the server from handling other incoming requests efficiently. In JavaScript, using async/await with Promises ensures that tasks run asynchronously, allowing the server to continue processing other requests without being halted.


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

@Injectable()
export class AppService {
  async getData(): Promise<string> {
    return new Promise((resolve) => {
      setTimeout(() => resolve('Hello, world!'), 1000);
    });
  }
}

Avoid CPU-intensive synchronous operations that block the event loop, as seen in the example below:


// Bad practice: Blocks the event loop
for (let i = 0; i < 1e9; i++) {}

Instead of executing heavy computations directly within the API, consider offloading them to worker threads or background jobs. This ensures that your API remains responsive even when handling computationally intensive tasks.

2. Optimize Database Queries

Database performance plays a crucial role in the scalability of an API. Poorly optimized queries can become a major bottleneck, significantly slowing down response times. One of the best practices for improving query efficiency is to use indexing. Indexes allow the database to retrieve data more quickly, reducing the time required to search through large datasets.


CREATE INDEX idx_user_email ON users (email);

Additionally, avoid using SELECT * queries, as they fetch all columns in a table, consuming unnecessary resources. Instead, retrieve only the required fields:


// Bad practice: Fetches all data
const users = await prisma.user.findMany();

// Optimized query: Fetches only specific fields
const users = await prisma.user.findMany({
  select: { id: true, name: true, email: true },
});

Caching is another powerful technique to reduce database load. By storing frequently accessed data in a caching layer like Redis, you can serve repeated requests without querying the database each time:


import { Redis } from 'ioredis';

const redis = new Redis();

async function getCachedUser(id: string) {
  const cachedUser = await redis.get(`user:${id}`);
  if (cachedUser) return JSON.parse(cachedUser);

  const user = await prisma.user.findUnique({ where: { id } });
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600); // Cache for 1 hour
  return user;
}

3. Rate Limiting to Prevent Abuse

APIs are often vulnerable to traffic overloads, whether from malicious attacks or excessive user requests. Without a proper rate-limiting mechanism, bots and spammers can flood your API with millions of requests, causing server instability. Implementing rate-limiting helps prevent such abuse and ensures that your API remains accessible to legitimate users.

Read Also: Golang Best practices for improved performance
 

To get started, install the rate-limiting middleware:


npm install express-rate-limit

Then, apply rate limits in your Nest.js application:


import * as rateLimit from 'express-rate-limit';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

  app.use(
    rateLimit({
      windowMs: 60 * 1000, // 1 minute
      max: 100, // Limit each IP to 100 requests per minute
    }),
  );

  await app.listen(3000);
}
bootstrap();

4. Use Load Balancing with Multiple Instances

A single server cannot efficiently handle millions of requests on its own. Instead, distributing traffic across multiple instances of your API ensures better performance and scalability. One way to achieve this is by using PM2, a process manager that allows you to run multiple instances of your Nest.js application:


npm install pm2 -g
pm2 start dist/main.js -i max

Additionally, using a reverse proxy like Nginx helps distribute requests across multiple backend instances:


upstream nest_api {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

server {
    listen 80;
    location / {
        proxy_pass http://nest_api;
    }
}

5. Offload Heavy Tasks to Background Jobs

APIs should focus on handling user requests efficiently, rather than processing time-consuming tasks. Heavy computations, data processing, and long-running operations should be offloaded to background jobs using tools like BullMQ.

Start by installing BullMQ and Redis:


npm install bullmq ioredis

Then, create a task queue for processing jobs asynchronously:


import { Queue, Worker } from 'bullmq';
import { redisConfig } from './redis.config';

const taskQueue = new Queue('tasks', { connection: redisConfig });

new Worker('tasks', async (job) => {
  console.log(`Processing task: ${job.id}`);
  await new Promise((res) => setTimeout(res, 5000)); // Simulate delay
  console.log(`Task completed: ${job.id}`);
});

export { taskQueue };

Finally Scale Like a Pro

Scaling a Nest.js API to handle millions of requests requires strategic optimizations across multiple layers. By implementing asynchronous code, optimizing database queries, enforcing rate limits, balancing server load, and offloading tasks to background workers, you can ensure that your API remains fast, reliable, and highly available. These best practices will help you build an API that can scale efficiently while maintaining a smooth user experience. Now, you’re ready to take your Nest.js API to the next level!

Enjoyed this post? Never miss out on future posts on Tech Tutorials Hub by «following us on WhatsApp» or Download our App
Views

Post a Comment

Thanks for reading, we would love to know if this was helpful. Don't forget to share!