· frameworks  · 6 min read

Boost Your NestJS Performance: 5 Tips You Didn't Know You Needed

Discover five lesser-known NestJS optimizations-from Fastify+fast-json-stringify to avoiding request-scoped providers-that deliver measurable performance wins for real production apps.

Discover five lesser-known NestJS optimizations-from Fastify+fast-json-stringify to avoiding request-scoped providers-that deliver measurable performance wins for real production apps.

Outcome: make your NestJS app faster, more efficient, and easier to scale. Read on and you’ll walk away with five actionable optimizations you can apply today to cut latency, reduce CPU work, and lower memory churn.

Why this matters up front: small per-request costs multiply quickly. One slow JSON serializer, one request-scoped provider, or one expensive validation pipeline can turn thousands of requests into a CPU bottleneck. Fix those, and throughput rises without changing your business logic.

1) Replace Express with Fastify + fast-json-stringify for far faster JSON

Short: swap adapters. Big payoff.

Nest ships with Express by default. Express is familiar, stable, and feature-rich - but when raw JSON throughput matters, Fastify is demonstrably faster thanks to its optimized request/response pipeline and optional fast-json-stringify integration.

Why it’s faster

  • Fastify’s routing and serialization avoid some reflection and allocations that Express incurs.
  • fast-json-stringify can compile a JSON schema to a serializer, reducing stringify overhead on large payloads.

How to do it

// main.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter({ logger: true })
  );

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

For heavy serialization routes, you can register a precompiled serializer via Fastify route options or create a custom interceptor that uses fast-json-stringify for a known DTO shape.

References: Nest Fastify guidance and the serializer project:

When to use

Use this when your JSON responses are large or your app needs the best possible per-request throughput.

Caveat

Fastify plugins differ from Express middleware, so test middleware and third-party integrations.

2) Avoid request-scoped providers unless you absolutely need them

Short: request-scoped providers are convenient - but expensive.

Nest supports provider scopes: singleton (default), transient, and request. A request-scoped provider is instantiated per incoming request, and Nest uses its DI container and context creation for each instantiation. That per-request work adds CPU and memory churn.

Why it hurts

  • Extra allocations per request.
  • Reflection and context creation overhead in the DI container.

What to do instead

  • Prefer singletons and pass request data explicitly to methods.
  • If you need per-request state (e.g., auth session), extract it into a plain object and pass it as a parameter.
  • Use AsyncLocalStorage (Node 13+) or a small context utility sparingly when necessary - but measure first.

Example - avoid request-scoped provider:

// BAD: request-scoped
@Injectable({ scope: Scope.REQUEST })
export class RequestService {
  constructor(@Inject(REQUEST) private readonly req) {}
  getUser() {
    return this.req.user;
  }
}

// BETTER: singleton service, request passed explicitly
@Injectable()
export class AuthService {
  getUserFromRequest(req: Request) {
    return req.user;
  }
}

References:

When to break the rule

If you have a small number of endpoints and the convenience outweighs performance cost, request scope is fine. Otherwise, avoid it in hot paths.

3) Re-think class-transformer/class-validator for high-throughput endpoints

Short: validation and transformation can be surprisingly costly - pick the right tool for the job.

Nest’s ValidationPipe and class-transformer are powerful and convenient, but transform: true and heavy DTO transformation incur object allocations and reflective work. For high-frequency or large-body endpoints consider alternatives.

Options

  • Only enable transform where you need it. Use whitelist: true and forbidNonWhitelisted: true for security without always transforming types.
  • Replace or complement class-validator with schema-based validators like Zod or Joi for faster and more predictable validation.
  • Perform minimal parsing at the controller boundary and push heavy DTO construction later where it can be batched or cached.

Example: lighter ValidationPipe usage

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    transform: false, // avoid class-transformer unless needed
  })
);

Example: using Zod at the controller edge (JS/TS)

import { z } from 'zod';

const createSchema = z.object({ name: z.string(), age: z.number() });

@Post()
create(@Body() body: any) {
  const parsed = createSchema.parse(body); // very fast and inlined
  return this.service.create(parsed);
}

References:

When to use which

  • Use class-validator/class-transformer when you rely on decorators and DI in DTOs.
  • Use Zod/Joi for hot endpoints where speed and predictable allocations matter.

4) Batch DB calls (DataLoader) and cache smartly to avoid N+1s

Short: eliminate repeated work per request.

N+1 database queries and microservice calls are common performance killers. Nest apps using ORMs (TypeORM, Sequelize) or Prisma, or heavy GraphQL resolvers can benefit from batching and caching.

Practical techniques

  • Use DataLoader (https://github.com/graphql/dataloader) to batch and cache identical DB calls during a single request lifecycle.
  • Add a Redis-backed cache for expensive computations or frequently-requested resources using Nest’s cache manager and @CacheInterceptor.
  • Cache at the HTTP edge (CDN) where appropriate.

Example: simple DataLoader integration

// user.loader.ts
import DataLoader from 'dataloader';

export function createUserLoader(userService: UserService) {
  return new DataLoader(async (ids: readonly number[]) => {
    const users = await userService.findByIds(ids as number[]);
    // return users ordered by ids
    const map = new Map(users.map(u => [u.id, u]));
    return ids.map(id => map.get(id));
  });
}

Example: Redis cache

// app.module.ts
CacheModule.register({
  store: redisStore, // via cache-manager-redis-store
  host: 'localhost',
  port: 6379,
});

References:

When to use

Batching and caching shine when you see many repeated calls per request or expensive joins. Measure first; caches can add complexity.

5) Tune Node and the HTTP server: keep-alive, thread pool, and proper production flags

Short: small Node and server tweaks give reliable wins under real load.

A few process-level and server settings often get overlooked but affect tail latency and throughput.

Quick checklist

  • Use keep-alive and connection pooling. For external HTTP calls, reuse HTTP agents. For incoming traffic, ensure proxies and load balancers use keep-alive.
  • Increase the libuv thread pool if you do CPU-bound or heavy filesystem/crypto work by setting UV_THREADPOOL_SIZE.
  • Run a compiled production build (tsc -> node dist/main.js). Avoid ts-node in production.
  • Use modern Node LTS releases (newer V8 => faster JS). Test with production flags when necessary: e.g., --max-old-space-size for memory-limited environments.
  • Use a process manager (PM2, systemd) and run multiple workers/instances to leverage multi-core. For CPU-bound workloads, use cluster mode.

Example – Node env + threadpool

# set a larger threadpool for heavy IO/crypto
export UV_THREADPOOL_SIZE=16
export NODE_ENV=production
node dist/main.js

References:

When to tune

If CPU or latency spikes appear under load tests, these OS/Node-level knobs often provide immediate relief.

Putting it together: measure, change one thing at a time, and benchmark

Do not apply all five changes blindly. Start by benchmarking your current app (Autocannon, wrk, k6). Pick the biggest offender and fix it. Rerun benchmarks. Repeat.

  • Measure: use realistic load and profile CPU/heap.
  • Change one thing: avoid confounding variables.
  • Re-measure: confirm impact.

Final thought (and the strongest point): the largest wins in NestJS often come from architectural choices - the adapter, provider scopes, and how you validate and access data - not from micro-optimizations. Fix the architecture, and the rest follows.

References and further reading

Back to Blog

Related Posts

View All Posts »
Mastering Hapi.js: 10 Tips to Boost Your API Performance

Mastering Hapi.js: 10 Tips to Boost Your API Performance

Learn 10 practical, high-impact techniques to make your Hapi.js APIs faster, leaner, and more scalable. Covers Catbox caching, response ETags, streaming, database patterns, validation tuning, plugin design, metrics, clustering, and avoiding event-loop blocking.

Maximizing Performance: Astro's Secret Weapon

Maximizing Performance: Astro's Secret Weapon

Discover lesser-known Astro optimizations - from smart hydration directives to critical CSS inlining, image strategies, and build-time tricks - that can make your static sites feel instant.