Node.jsNestJSTypeScriptExpressMicroservices

Node.js & NestJS Interview — The Complete Guide

Everything you need for Node.js and NestJS rounds at product companies. Event loop internals, streams, NestJS architecture, TypeORM, guards, pipes, microservices, testing, and the questions that actually get asked.

160 min read13 sections
01

Node.js Internals — Event Loop & V8

🔥 This is the #1 Node.js interview topic

The event loop is asked in every single Node.js interview. Understand the phases, the difference from the browser event loop, and be able to predict output order for code snippets. This alone covers 40% of Node interview questions.

🔥 Event Loop — The 6 Phases

Node.js uses libuv to implement its event loop. Unlike the browser's simpler model, Node's loop has six distinct phases, each with its own callback queue. Understanding the order is critical for predicting execution behavior.

  1. Timers — executes setTimeout and setInterval callbacks
  2. Pending callbacks — I/O callbacks deferred from the previous loop (e.g., TCP errors)
  3. Idle / Prepare — internal use only
  4. Poll — retrieves new I/O events, executes I/O callbacks (file reads, network)
  5. Check — executes setImmediate callbacks
  6. Close callbackssocket.on('close'), cleanup handlers

Between every phase, Node drains the microtask queue (process.nextTick first, then Promises). This is whynextTick has the highest priority of any async callback.

event-loop-order.tstypescript
console.log("1 — sync");

setTimeout(() => console.log("2 — timer phase"), 0);
setImmediate(() => console.log("3 — check phase"));

process.nextTick(() => console.log("4 — nextTick (microtask)"));
Promise.resolve().then(() => console.log("5 — Promise (microtask)"));

console.log("6 — sync");

// Output: 1, 6, 4, 5, 2, 3
// Sync first → nextTick → Promise → timer → immediate
// Note: timer vs immediate order can vary inside I/O callbacks

🔥 Node.js vs Browser Event Loop

FeatureNode.jsBrowser
Event loop6 phases (libuv)Task → microtasks → render
process.nextTick✅ Highest priority microtask❌ Not available
setImmediate✅ Check phase❌ Not standard
I/O handlinglibuv thread pool (fs, dns, crypto)Web APIs (fetch, setTimeout)
ThreadsWorker Threads (manual)Web Workers
Microtask drainBetween every phaseAfter every macrotask

🔥 process.nextTick vs setImmediate vs setTimeout

APIWhen it runsPriorityUse case
process.nextTickBefore any I/O or timer, after current operationHighestMust run before anything else (error handling, cleanup)
Promise.thenAfter nextTick, before timersHighAsync continuations
setTimeout(fn, 0)Timer phase (next iteration)MediumDefer to next loop iteration
setImmediateCheck phase (after I/O poll)MediumRun after I/O events are processed

💡 The classic interview trap

"What's the order of setTimeout(0) vs setImmediate?" — From the main module, the order is non-deterministic (depends on process performance). Inside an I/O callback, setImmediate always fires first because the check phase comes right after the poll phase.

V8 Engine & Memory

V8 compiles JavaScript to machine code (JIT compilation). Memory is divided into the heap (objects, closures) and the stack (function calls, primitives). V8's garbage collector uses generational collection: young generation (Scavenge — fast, frequent) and old generation (Mark-Sweep-Compact — slower, less frequent). Memory leaks happen when references to unused objects are retained — common culprits: global variables, closures, event listeners not removed, and growing arrays/maps.

memory-debugging.tstypescript
// Check memory usage
const used = process.memoryUsage();
console.log({
  rss: `${Math.round(used.rss / 1024 / 1024)} MB`,        // total allocated
  heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`, // actual usage
  heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`, // V8 heap size
});

// Common memory leak: event listeners not cleaned up
const emitter = new EventEmitter();
function handler() { /* ... */ }
emitter.on("data", handler);
// Later: emitter.removeListener("data", handler); // MUST clean up

// V8 heap limit (default ~1.5GB)
// Increase with: node --max-old-space-size=4096 app.js

libuv Thread Pool

Not all I/O is truly async at the OS level. File system operations, DNS lookups, and crypto use libuv's thread pool (default 4 threads). Network I/O (TCP, HTTP) uses OS-level async APIs (epoll, kqueue). If you're doing heavy file I/O or crypto, increase the pool: UV_THREADPOOL_SIZE=16.

📝 Quick Revision

Quick Revision Cheat Sheet

Event loop: 6 phases: timers → pending → idle → poll → check → close. Microtasks drain between phases.

nextTick: Highest priority. Runs before any I/O or timer. Use sparingly — can starve I/O.

setImmediate: Check phase. Always fires before setTimeout(0) inside I/O callbacks.

V8 memory: Heap (objects) + stack (calls). Generational GC. Default ~1.5GB limit.

Thread pool: 4 threads default (UV_THREADPOOL_SIZE). Used for fs, dns, crypto. Not for network I/O.

Common Interview Questions

Q:How does Node.js handle concurrent requests if it's single-threaded?

A: Node delegates I/O to libuv (thread pool for fs/dns/crypto, OS async APIs for network). The main thread registers callbacks and continues processing. When I/O completes, callbacks are queued in the event loop. This lets one thread handle thousands of concurrent connections — it's not doing the I/O work itself.

Q:What's the difference between process.nextTick and Promise.then?

A: Both are microtasks, but nextTick has higher priority — the nextTick queue is drained completely before the Promise microtask queue. In practice: nextTick for 'must run before anything else', Promise.then for normal async continuations. Overusing nextTick can starve I/O because it runs before any I/O callbacks.

02

Streams, Buffers & File I/O

Streams are Node's superpower for handling large data efficiently. Instead of loading a 2GB file into memory, you process it in chunks. This is how Node handles file uploads, HTTP responses, database result sets, and real-time data.

🔥 Stream Types

TypeWhat it doesExample
ReadableProduces data you can consumefs.createReadStream, HTTP request, process.stdin
WritableConsumes data you write to itfs.createWriteStream, HTTP response, process.stdout
DuplexBoth readable and writableTCP socket, WebSocket
TransformModifies data as it passes throughzlib.createGzip, crypto.createCipher
streams.tstypescript
import { createReadStream, createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { createGzip } from "zlib";
import { Transform } from "stream";

// Basic: stream a file through gzip to output
await pipeline(
  createReadStream("input.log"),       // Readable
  createGzip(),                         // Transform
  createWriteStream("output.log.gz")   // Writable
);
// Memory stays constant regardless of file size

// Custom transform stream
const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

await pipeline(
  createReadStream("input.txt"),
  upperCase,
  createWriteStream("output.txt")
);

🔥 Backpressure — Must Know

When a readable stream produces data faster than the writable stream can consume it, backpressure builds up. Without handling it, data buffers in memory and can crash the process. pipeline() handles backpressure automatically — it pauses the readable when the writable's internal buffer is full and resumes when it drains. Never use .pipe() without error handling — use pipeline() from stream/promises instead.

backpressure.tstypescript
// ❌ Bad — .pipe() doesn't handle errors or backpressure properly
readStream.pipe(transformStream).pipe(writeStream);

// ✅ Good — pipeline handles backpressure + errors + cleanup
import { pipeline } from "stream/promises";
try {
  await pipeline(readStream, transformStream, writeStream);
  console.log("Pipeline complete");
} catch (err) {
  console.error("Pipeline failed:", err);
}

// Manual backpressure handling (for understanding)
readStream.on("data", (chunk) => {
  const canContinue = writeStream.write(chunk);
  if (!canContinue) {
    readStream.pause();                    // stop reading
    writeStream.once("drain", () => {
      readStream.resume();                 // resume when buffer drains
    });
  }
});

Buffers

Buffers are fixed-size chunks of raw binary data — the building blocks streams operate on. They exist outside V8's heap, so they don't count toward the heap memory limit. Use them for binary data (images, files, network packets), not for string manipulation.

buffers.tstypescript
// Creating buffers
const buf1 = Buffer.from("Hello, World!");          // from string
const buf2 = Buffer.alloc(1024);                     // zero-filled, 1KB
const buf3 = Buffer.allocUnsafe(1024);               // uninitialized (faster, may contain old data)

// Buffer operations
buf1.toString("utf-8");                              // "Hello, World!"
buf1.toString("base64");                             // "SGVsbG8sIFdvcmxkIQ=="
Buffer.concat([buf1, buf2]);                         // combine buffers
buf1.length;                                         // 13 bytes (not characters!)

// ⚠️ Buffer.allocUnsafe is faster but may contain old memory data
// Only use when you'll immediately overwrite the entire buffer

File System — Async Patterns

fs-patterns.tstypescript
import { readFile, writeFile, stat, mkdir } from "fs/promises";
import { existsSync } from "fs";

// ✅ Always use fs/promises (async) — never fs sync methods in production
const content = await readFile("config.json", "utf-8");
const config = JSON.parse(content);

await writeFile("output.json", JSON.stringify(data, null, 2));

// Check if file exists (sync is OK for startup checks)
if (existsSync("./uploads")) {
  const stats = await stat("./uploads");
  console.log("Is directory:", stats.isDirectory());
}

// ❌ Never in production — blocks the event loop
// const data = readFileSync("large-file.csv", "utf-8");

📝 Quick Revision

Quick Revision Cheat Sheet

Streams: Readable, Writable, Duplex, Transform. Process data in chunks, constant memory.

pipeline(): Use over .pipe(). Handles backpressure, errors, and cleanup automatically.

Backpressure: Readable faster than writable → buffer grows. pipeline() handles it.

Buffers: Raw binary data. Outside V8 heap. Use for binary, not strings.

fs/promises: Always async. Never use sync methods in production (blocks event loop).

03

Module System & Package Management

Node.js has two module systems: CommonJS (the original) and ES Modules (the standard). Understanding the differences, how resolution works, and the caching behavior is a frequent interview topic.

🔥 CommonJS vs ES Modules

FeatureCommonJS (CJS)ES Modules (ESM)
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
EvaluationRuntime (dynamic)Static (parsed at compile time)
Tree shaking❌ Not possible✅ Dead code elimination
Top-level await❌ Not supported✅ Supported
File extension.js (default) or .cjs.mjs or .js with type: module
this at top levelmodule.exportsundefined
modules.tstypescript
// CommonJS
const express = require("express");
module.exports = { handler };
module.exports.helper = helper;

// ES Modules
import express from "express";
export { handler };
export default handler;

// Dynamic import (works in both)
const module = await import("./heavy-module.js");

// CommonJS in ESM: use createRequire
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const pkg = require("./package.json");

🔥 Module Caching

When you require() a module, Node caches it in require.cache. Subsequent require() calls return the cached export — the module code runs only once. This is why singletons work naturally in Node: export an instance, and every file that imports it gets the same object. ES Modules have the same caching behavior — modules are evaluated once and cached.

Module Resolution

When you require('express'), Node looks in: (1) core modules (fs, path, http), (2) node_modules/ in the current directory, (3) parent node_modules/, walking up to the root. For relative paths (./utils), it tries: exact file → .js → .json → .node → index.js in directory.

package.json Essentials

FieldPurposeInterview relevance
dependenciesProduction packagesInstalled with npm install
devDependenciesDev-only packages (test, lint, build)Not installed in production (npm install --production)
peerDependenciesExpected to be provided by the consumerPlugin systems, shared libraries
enginesRequired Node.js versionPrevents running on incompatible versions
type: moduleEnables ES Modules for .js filesWithout it, .js files use CommonJS

📝 Quick Revision

Quick Revision Cheat Sheet

CJS vs ESM: CJS: require/module.exports, sync, runtime. ESM: import/export, async, static, tree-shakeable.

Module cache: Modules run once, cached. require.cache stores them. Singletons work naturally.

Resolution: Core → node_modules (walk up) → exact file → .js → .json → index.js.

peerDependencies: Consumer must provide. Used by plugins and shared libraries.

04

Error Handling & Debugging

Unhandled errors crash Node processes. Unlike browsers where an error shows in the console and life goes on, a Node server that throws an unhandled exception dies. Robust error handling is not optional — it's a production requirement.

🔥 Error Handling Patterns

error-handling.tstypescript
// 1. Async/await — always wrap in try/catch
async function fetchUser(id: string) {
  try {
    const user = await db.findById(id);
    if (!user) throw new NotFoundError("User not found");
    return user;
  } catch (err) {
    if (err instanceof NotFoundError) throw err; // re-throw known errors
    logger.error("Unexpected error fetching user", { id, err });
    throw new InternalError("Failed to fetch user");
  }
}

// 2. Promise chains — always have a .catch()
fetchData()
  .then(process)
  .catch((err) => logger.error("Pipeline failed", err));

// 3. Event emitters — listen for 'error' event
const stream = fs.createReadStream("file.txt");
stream.on("error", (err) => {
  logger.error("Stream error", err);
});

// 4. Global safety nets (last resort — log and restart)
process.on("uncaughtException", (err) => {
  logger.fatal("Uncaught exception", err);
  process.exit(1); // exit — state may be corrupted
});

process.on("unhandledRejection", (reason) => {
  logger.fatal("Unhandled rejection", reason);
  process.exit(1);
});

🔥 Custom Error Classes

custom-errors.tstypescript
// Base application error
class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public isOperational = true // operational vs programmer error
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(message = "Resource not found") {
    super(404, "NOT_FOUND", message);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public details?: Record<string, string>[]) {
    super(400, "VALIDATION_ERROR", message);
  }
}

class UnauthorizedError extends AppError {
  constructor(message = "Unauthorized") {
    super(401, "UNAUTHORIZED", message);
  }
}

// Operational errors: expected failures (bad input, not found, auth)
// Programmer errors: bugs (TypeError, null reference) — crash and restart

Operational vs Programmer Errors

TypeExamplesHow to handle
OperationalInvalid input, not found, timeout, connection refusedHandle gracefully — return error response, retry, fallback
ProgrammerTypeError, null reference, wrong argument typeCrash and restart — state may be corrupted. Fix the bug.

Debugging Tools

  • node --inspect app.js — Chrome DevTools debugger
  • node --inspect-brk app.js — break on first line
  • console.time() / console.timeEnd() — measure execution time
  • node --prof app.js — CPU profiling
  • node --heap-prof app.js — heap snapshots for memory leaks
  • Clinic.js — production-grade profiling (flame graphs, event loop delays)

📝 Quick Revision

Quick Revision Cheat Sheet

Async errors: Always try/catch with async/await. Always .catch() on promises.

Streams: Listen for 'error' event. Use pipeline() for automatic error handling.

Global handlers: uncaughtException and unhandledRejection — log and exit. Last resort.

Operational vs programmer: Operational: handle gracefully. Programmer: crash and restart.

Debugging: --inspect for Chrome DevTools. --prof for CPU. --heap-prof for memory.

05

Express.js Essentials

Express is the most widely used Node.js web framework. Even if you use NestJS (which uses Express or Fastify under the hood), understanding Express middleware, routing, and error handling is expected in interviews.

🔥 Middleware — The Core Concept

Everything in Express is middleware. A middleware function receives (req, res, next) and either responds or calls next() to pass control to the next middleware. The order you register middleware matters — they execute top to bottom.

middleware.tstypescript
import express from "express";
const app = express();

// 1. Built-in middleware
app.use(express.json());                    // parse JSON bodies
app.use(express.urlencoded({ extended: true })); // parse form data

// 2. Custom middleware — runs for every request
app.use((req, res, next) => {
  req.requestId = crypto.randomUUID();
  console.log(`[${req.method}] ${req.path}${req.requestId}`);
  next(); // MUST call next() or the request hangs
});

// 3. Route-specific middleware
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "No token" });
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
};

app.get("/api/profile", authenticate, (req, res) => {
  res.json(req.user); // only runs if authenticate calls next()
});

// 4. Error-handling middleware (4 params — Express recognizes the signature)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.statusCode || 500).json({
    error: { code: err.code || "INTERNAL_ERROR", message: err.message },
  });
});

Middleware Execution Order

Request flows through middleware in registration order: global middleware → route-specific middleware → route handler → error middleware. If any middleware doesn't call next() and doesn't send a response, the request hangs forever. Error middleware must have 4 parameters — Express uses the arity to identify it.

Express vs Fastify

AspectExpressFastify
PerformanceGood (~15K req/s)Excellent (~75K req/s)
EcosystemMassive — most middleware availableGrowing — compatible with Express middleware via plugin
ValidationManual or third-party (Joi, Zod)Built-in JSON Schema validation
TypeScriptNeeds @types/expressFirst-class TypeScript support
LoggingManual (morgan, winston)Built-in Pino logger
NestJSDefault HTTP adapterSupported as alternative adapter

📝 Quick Revision

Quick Revision Cheat Sheet

Middleware: (req, res, next) — call next() or respond. Order matters.

Error middleware: 4 params: (err, req, res, next). Register last.

express.json(): Parses JSON request bodies. Must be registered before routes.

Fastify: 5x faster than Express. Built-in validation and logging. NestJS supports both.

06

NestJS Architecture & Core Concepts

🔥 NestJS is the Spring Boot of Node.js

NestJS brings structure, dependency injection, and decorators to Node.js. If you know Spring Boot, NestJS will feel familiar — modules, providers, controllers, and a DI container. It's the most popular enterprise Node.js framework.

🔥 Core Building Blocks

ConceptWhat it isSpring Boot equivalent
ModuleOrganizes related code. Every app has a root AppModule.@Configuration / @SpringBootApplication
ControllerHandles HTTP requests. Decorated with @Controller.@RestController
Provider / ServiceBusiness logic. Injectable via DI.@Service
MiddlewareRuns before route handler. Like Express middleware.Filter (Servlet level)
GuardDecides if request should proceed (auth/roles).Spring Security filter
PipeTransforms or validates input data.@Valid + Bean Validation
InterceptorWraps execution (logging, caching, transform response).HandlerInterceptor / AOP @Around
Exception FilterCatches and formats exceptions.@ControllerAdvice

🔥 Module System

Every NestJS app is organized into modules. A module is a class decorated with @Module() that groups related controllers, providers, and imports. The root AppModule imports all feature modules. Modules encapsulate their providers — a service inUserModule isn't accessible from OrderModule unless explicitly exported.

modules.tstypescript
// user.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User])],  // register entities
  controllers: [UserController],                 // HTTP handlers
  providers: [UserService],                      // business logic
  exports: [UserService],                        // make available to other modules
})
export class UserModule {}

// app.module.ts — root module
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),    // global config
    TypeOrmModule.forRoot({ /* DB config */ }),   // database
    UserModule,                                   // feature module
    OrderModule,
    AuthModule,
  ],
})
export class AppModule {}

🔥 Controllers

user.controller.tstypescript
@Controller("users")
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll(@Query("page") page = 1, @Query("limit") limit = 20) {
    return this.userService.findAll(page, limit);
  }

  @Get(":id")
  findOne(@Param("id", ParseIntPipe) id: number) {
    return this.userService.findOne(id);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @UsePipes(new ValidationPipe({ whitelist: true }))
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }

  @Put(":id")
  update(@Param("id", ParseIntPipe) id: number,
         @Body() updateUserDto: UpdateUserDto) {
    return this.userService.update(id, updateUserDto);
  }

  @Delete(":id")
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param("id", ParseIntPipe) id: number) {
    return this.userService.remove(id);
  }
}

🔥 Dependency Injection

NestJS has a built-in IoC container. Any class decorated with @Injectable() can be injected via constructor injection. The container resolves the dependency graph automatically. Scopes:DEFAULT (singleton — one instance per module), REQUEST (new instance per request), TRANSIENT (new instance per injection).

di-example.tstypescript
// Service — @Injectable makes it available for DI
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private userRepo: Repository<User>,
    private readonly emailService: EmailService, // auto-injected
  ) {}

  async findOne(id: number): Promise<User> {
    const user = await this.userRepo.findOneBy({ id });
    if (!user) throw new NotFoundException(`User ${id} not found`);
    return user;
  }
}

// Custom provider — useful for third-party libraries
@Module({
  providers: [
    {
      provide: "REDIS_CLIENT",
      useFactory: async (config: ConfigService) => {
        return new Redis(config.get("REDIS_URL"));
      },
      inject: [ConfigService],
    },
  ],
})
export class CacheModule {}

// Inject custom provider
@Injectable()
export class CacheService {
  constructor(@Inject("REDIS_CLIENT") private redis: Redis) {}
}

Request Lifecycle

The full NestJS request pipeline: Middleware GuardsInterceptors (before) PipesRoute handler Interceptors (after)Exception filters. Each layer has a specific responsibility. Understanding this order is critical for debugging and for interview questions.

📝 Quick Revision

Quick Revision Cheat Sheet

Module: Groups controllers + providers. Encapsulates scope. Export to share.

Controller: Handles HTTP. @Get, @Post, @Put, @Delete. Delegates to services.

Provider: @Injectable. Business logic. Injected via constructor.

DI scopes: DEFAULT (singleton), REQUEST (per request), TRANSIENT (per injection).

Request lifecycle: Middleware → Guard → Interceptor → Pipe → Handler → Interceptor → Exception filter.

07

NestJS Providers, Pipes & Guards

Pipes, guards, and interceptors are the middleware layers that make NestJS powerful. Each has a single responsibility — validation, authorization, or cross-cutting concerns. Interviewers test whether you know when to use each.

🔥 Pipes — Validation & Transformation

Pipes run before the route handler. They either transform input data (e.g., string to number) or validate it (throw 400 if invalid). NestJS ships with built-in pipes: ValidationPipe, ParseIntPipe, ParseBoolPipe, ParseUUIDPipe.

validation.tstypescript
// DTO with class-validator decorators
import { IsEmail, IsNotEmpty, MinLength, IsOptional, Min } from "class-validator";

export class CreateUserDto {
  @IsNotEmpty({ message: "Name is required" })
  name: string;

  @IsEmail({}, { message: "Must be a valid email" })
  email: string;

  @MinLength(8, { message: "Password must be at least 8 characters" })
  password: string;

  @IsOptional()
  @Min(18, { message: "Must be at least 18" })
  age?: number;
}

// Global validation pipe — validates all incoming DTOs
// main.ts
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,        // strip unknown properties
  forbidNonWhitelisted: true, // throw if unknown properties sent
  transform: true,        // auto-transform payloads to DTO instances
}));

// Controller — @Body() is automatically validated
@Post()
create(@Body() dto: CreateUserDto) {
  return this.userService.create(dto);
}
// If validation fails → 400 { statusCode: 400, message: [...errors] }

Custom Pipes

custom-pipe.tstypescript
// Custom pipe — parse and validate ObjectId
@Injectable()
export class ParseObjectIdPipe implements PipeTransform<string> {
  transform(value: string): string {
    if (!Types.ObjectId.isValid(value)) {
      throw new BadRequestException(`"${value}" is not a valid ObjectId`);
    }
    return value;
  }
}

// Usage
@Get(":id")
findOne(@Param("id", ParseObjectIdPipe) id: string) {
  return this.service.findOne(id);
}

🔥 Guards — Authorization

Guards determine whether a request should be handled. They run after middleware but before pipes and interceptors. Return true to allow, false or throw to deny. Use for authentication, role checks, and feature flags.

guards.tstypescript
// JWT Auth Guard
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(" ")[1];

    if (!token) throw new UnauthorizedException("No token provided");

    try {
      const payload = this.jwtService.verify(token);
      request.user = payload;
      return true;
    } catch {
      throw new UnauthorizedException("Invalid token");
    }
  }
}

// Roles Guard — checks user role
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>("roles", context.getHandler());
    if (!requiredRoles) return true; // no roles required

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

// Custom decorator for roles
const Roles = (...roles: string[]) => SetMetadata("roles", roles);

// Usage
@Get("admin/dashboard")
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles("admin")
getAdminDashboard() {
  return this.adminService.getDashboard();
}

🔥 Interceptors — Cross-Cutting Concerns

interceptors.tstypescript
// Logging interceptor — measures request duration
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest();
    const start = Date.now();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        console.log(`[${request.method}] ${request.url}${duration}ms`);
      }),
    );
  }
}

// Transform interceptor — wrap all responses in { data: ... }
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
  intercept(context: ExecutionContext, next: CallHandler<T>) {
    return next.handle().pipe(
      map((data) => ({ data, timestamp: new Date().toISOString() })),
    );
  }
}

// Apply globally
app.useGlobalInterceptors(new LoggingInterceptor());

Exception Filters

exception-filter.tstypescript
@Catch() // catches all exceptions
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = exception instanceof HttpException
      ? exception.getResponse()
      : "Internal server error";

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

// Apply globally
app.useGlobalFilters(new AllExceptionsFilter());

📝 Quick Revision

Quick Revision Cheat Sheet

Pipes: Validate + transform input. ValidationPipe with class-validator DTOs.

Guards: Auth + roles. Return true/false. Run before pipes and handlers.

Interceptors: Before + after handler. Logging, caching, response transformation.

Exception filters: Catch and format errors. @Catch() for global handling.

Order: Middleware → Guard → Interceptor (before) → Pipe → Handler → Interceptor (after) → Filter.

08

Database Access — TypeORM & Prisma

NestJS integrates with multiple ORMs. TypeORM is the most common choice (decorator-based, feels natural with NestJS). Prisma is the modern alternative (schema-first, excellent type safety). Know both and their trade-offs.

🔥 TypeORM vs Prisma

AspectTypeORMPrisma
ApproachCode-first (decorators on classes)Schema-first (prisma.schema file)
TypeScriptGood — but types can drift from DBExcellent — auto-generated types from schema
MigrationsAuto-generate from entity changesprisma migrate dev from schema changes
Query builderQueryBuilder (chainable)Prisma Client (fluent API)
RelationsDecorators (@OneToMany, @ManyToOne)Defined in schema, auto-resolved
NestJS integrationFirst-class (@nestjs/typeorm)Good (@nestjs/prisma or manual)
Raw SQLquery() methodprisma.$queryRaw
Learning curveFamiliar if you know JPA/HibernateSimpler, less boilerplate

TypeORM — Entities & Repositories

typeorm-entity.tstypescript
// Entity definition
@Entity("users")
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @Column({ select: false }) // excluded from default queries
  password: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @OneToMany(() => Order, (order) => order.user)
  orders: Order[];
}

@Entity("orders")
export class Order {
  @PrimaryGeneratedColumn()
  id: number;

  @Column("decimal", { precision: 10, scale: 2 })
  total: number;

  @ManyToOne(() => User, (user) => user.orders)
  @JoinColumn({ name: "user_id" })
  user: User;

  @Column()
  userId: number;
}
typeorm-service.tstypescript
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private userRepo: Repository<User>,
  ) {}

  findAll(page: number, limit: number) {
    return this.userRepo.find({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: "DESC" },
    });
  }

  findOne(id: number) {
    return this.userRepo.findOne({
      where: { id },
      relations: ["orders"], // eager load orders
    });
  }

  // QueryBuilder for complex queries
  async findActiveWithOrders() {
    return this.userRepo
      .createQueryBuilder("user")
      .leftJoinAndSelect("user.orders", "order")
      .where("user.active = :active", { active: true })
      .andWhere("order.createdAt > :date", { date: thirtyDaysAgo })
      .orderBy("user.createdAt", "DESC")
      .getMany();
  }
}

Prisma — Schema & Client

schema.prismatypescript
// prisma/schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  password  String
  orders    Order[]
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@map("users")
}

model Order {
  id        Int      @id @default(autoincrement())
  total     Decimal  @db.Decimal(10, 2)
  user      User     @relation(fields: [userId], references: [id])
  userId    Int      @map("user_id")
  createdAt DateTime @default(now()) @map("created_at")

  @@map("orders")
}
prisma-service.tstypescript
@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}

  findAll(page: number, limit: number) {
    return this.prisma.user.findMany({
      skip: (page - 1) * limit,
      take: limit,
      orderBy: { createdAt: "desc" },
    });
  }

  findOne(id: number) {
    return this.prisma.user.findUnique({
      where: { id },
      include: { orders: true }, // eager load orders
    });
  }

  // Prisma transactions
  async createUserWithOrder(userData: CreateUserDto, orderData: CreateOrderDto) {
    return this.prisma.$transaction(async (tx) => {
      const user = await tx.user.create({ data: userData });
      const order = await tx.order.create({
        data: { ...orderData, userId: user.id },
      });
      return { user, order };
    });
  }
}

🔥 N+1 Problem in TypeORM

Same problem as JPA: loading users then accessing user.orders for each triggers N extra queries. Fix: use relations option in find(), or leftJoinAndSelect in QueryBuilder. In Prisma, use include to eager-load relations in a single query.

Migrations

ToolGenerateApplyRollback
TypeORMtypeorm migration:generatetypeorm migration:runtypeorm migration:revert
Prismaprisma migrate dev (auto from schema diff)prisma migrate deployprisma migrate reset (destructive)

📝 Quick Revision

Quick Revision Cheat Sheet

TypeORM: Code-first, decorators. QueryBuilder for complex queries. Familiar if you know JPA.

Prisma: Schema-first. Auto-generated types. Fluent API. Better type safety.

N+1 fix: TypeORM: relations option or leftJoinAndSelect. Prisma: include.

Transactions: TypeORM: queryRunner or @Transaction. Prisma: $transaction with callback.

Migrations: TypeORM: generate from entity diff. Prisma: generate from schema diff.

09

Authentication & Authorization

NestJS uses Passport.js for authentication strategies and its own guard system for authorization. The combination of JWT + Guards + custom decorators is the standard pattern at production companies.

🔥 Passport.js + JWT Strategy

jwt-strategy.tstypescript
// jwt.strategy.ts — validates JWT tokens
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: config.get("JWT_SECRET"),
    });
  }

  // Called after token is verified — return value is set as req.user
  async validate(payload: { sub: number; email: string; role: string }) {
    return { id: payload.sub, email: payload.email, role: payload.role };
  }
}

// auth.module.ts
@Module({
  imports: [
    PassportModule,
    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.get("JWT_SECRET"),
        signOptions: { expiresIn: "15m" },
      }),
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

🔥 Auth Service — Login & Token Management

auth.service.tstypescript
@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService,
  ) {}

  async login(email: string, password: string) {
    const user = await this.userService.findByEmail(email);
    if (!user) throw new UnauthorizedException("Invalid credentials");

    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) throw new UnauthorizedException("Invalid credentials");

    const payload = { sub: user.id, email: user.email, role: user.role };
    return {
      accessToken: this.jwtService.sign(payload),
      refreshToken: this.jwtService.sign(payload, { expiresIn: "7d" }),
    };
  }

  async refreshToken(token: string) {
    try {
      const payload = this.jwtService.verify(token);
      const newPayload = { sub: payload.sub, email: payload.email, role: payload.role };
      return { accessToken: this.jwtService.sign(newPayload) };
    } catch {
      throw new UnauthorizedException("Invalid refresh token");
    }
  }
}

🔥 Role-Based Access Control

rbac.tstypescript
// Custom decorator
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);

// Current user decorator
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

// Usage in controller
@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {

  @Get("users")
  @Roles("admin")
  getAllUsers(@CurrentUser() user: JwtPayload) {
    console.log("Requested by:", user.email);
    return this.adminService.getAllUsers();
  }

  @Delete("users/:id")
  @Roles("admin", "superadmin")
  deleteUser(@Param("id", ParseIntPipe) id: number) {
    return this.adminService.deleteUser(id);
  }
}

Refresh Token Strategy

Access tokens are short-lived (15 min). Refresh tokens are long-lived (7 days), stored server-side (Redis or DB), and used to get new access tokens. On logout, invalidate the refresh token. For immediate access token revocation, maintain a blocklist in Redis — but this adds statefulness.

📝 Quick Revision

Quick Revision Cheat Sheet

Passport.js: Strategy pattern for auth. JwtStrategy validates tokens. validate() sets req.user.

Guards: JwtAuthGuard for authentication. RolesGuard for authorization. Stack with @UseGuards.

Custom decorators: @Roles() for metadata. @CurrentUser() for extracting user from request.

Refresh tokens: Short access (15m) + long refresh (7d). Store refresh server-side. Invalidate on logout.

10

Testing in Node.js & NestJS

NestJS has first-class testing support with a dedicated @nestjs/testing package. It lets you create isolated testing modules with mocked dependencies — similar to Spring's test slices.

🔥 Testing Module — The Core Pattern

user.service.spec.tstypescript
describe("UserService", () => {
  let service: UserService;
  let repo: Repository<User>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useValue: {
            findOneBy: jest.fn(),
            save: jest.fn(),
            find: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get(UserService);
    repo = module.get(getRepositoryToken(User));
  });

  it("should return a user by id", async () => {
    const mockUser = { id: 1, name: "Alice", email: "alice@test.com" };
    jest.spyOn(repo, "findOneBy").mockResolvedValue(mockUser as User);

    const result = await service.findOne(1);
    expect(result).toEqual(mockUser);
    expect(repo.findOneBy).toHaveBeenCalledWith({ id: 1 });
  });

  it("should throw NotFoundException for missing user", async () => {
    jest.spyOn(repo, "findOneBy").mockResolvedValue(null);
    await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
  });
});

🔥 E2E Testing with Supertest

user.e2e-spec.tstypescript
describe("UserController (e2e)", () => {
  let app: INestApplication;

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = module.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
    await app.init();
  });

  afterAll(() => app.close());

  it("POST /users — should create a user", () => {
    return request(app.getHttpServer())
      .post("/users")
      .send({ name: "Alice", email: "alice@test.com", password: "password123" })
      .expect(201)
      .expect((res) => {
        expect(res.body).toHaveProperty("id");
        expect(res.body.email).toBe("alice@test.com");
      });
  });

  it("POST /users — should return 400 for invalid email", () => {
    return request(app.getHttpServer())
      .post("/users")
      .send({ name: "Alice", email: "not-an-email", password: "password123" })
      .expect(400);
  });

  it("GET /users/:id — should return 404 for missing user", () => {
    return request(app.getHttpServer())
      .get("/users/99999")
      .expect(404);
  });
});

Testing Strategy

LayerTest typeToolsWhat to test
ServiceUnit testJest + mock reposBusiness logic, edge cases, error handling
ControllerUnit testTest.createTestingModule + mock servicesRequest mapping, response shape
E2EIntegrationSupertest + full appFull request flow, validation, auth
RepositoryIntegrationTestcontainers or test DBCustom queries, migrations

Jest vs Vitest

AspectJestVitest
SpeedSlower (transforms with Babel/ts-jest)Faster (native ESM, Vite-powered)
NestJS supportFirst-class (default)Works but needs manual setup
Mockingjest.fn(), jest.spyOn()vi.fn(), vi.spyOn() (same API)
Configjest.config.tsvitest.config.ts
RecommendationUse for NestJS projects (default)Use for non-NestJS Node.js projects

📝 Quick Revision

Quick Revision Cheat Sheet

Test.createTestingModule: NestJS test factory. Provide mock dependencies. Compile and get services.

Unit tests: Mock repos/services. Test business logic in isolation. Fast.

E2E tests: Supertest + full app. Test HTTP flow, validation, auth. Slower.

Mocking: jest.fn() for stubs. jest.spyOn() for partial mocks. Provide mock in module.

11

Microservices & Message Queues

NestJS has built-in microservices support with multiple transport layers. You can build event-driven architectures with the same decorator-based approach used for HTTP controllers.

🔥 NestJS Transport Layers

TransportProtocolUse case
TCPCustom binary protocolInternal service-to-service (simplest)
RedisPub/SubLightweight messaging, real-time events
NATSLightweight messagingHigh-throughput, cloud-native
RabbitMQAMQPComplex routing, dead letter queues, reliability
KafkaDistributed logEvent streaming, audit logs, high throughput
gRPCHTTP/2 + ProtobufLow latency, type-safe, streaming

Message Patterns — Request/Response vs Event

microservice-patterns.tstypescript
// Request/Response — client sends, waits for reply
// Client side
@Injectable()
export class OrderService {
  constructor(@Inject("USER_SERVICE") private client: ClientProxy) {}

  async getUserForOrder(userId: number) {
    // send() returns Observable — use lastValueFrom to await
    return lastValueFrom(
      this.client.send({ cmd: "get_user" }, { userId })
    );
  }
}

// Server side (User microservice)
@Controller()
export class UserController {
  @MessagePattern({ cmd: "get_user" })
  getUser(@Payload() data: { userId: number }) {
    return this.userService.findOne(data.userId);
  }
}

// Event-based — fire and forget (no response)
// Client side
this.client.emit("order_placed", { orderId: 123, userId: 456 });

// Server side (Notification microservice)
@EventPattern("order_placed")
async handleOrderPlaced(@Payload() data: { orderId: number; userId: number }) {
  await this.notificationService.sendOrderConfirmation(data);
  // no return value — fire and forget
}

🔥 Bull Queue — Background Jobs

Bull (backed by Redis) is the standard for background job processing in NestJS. Use it for email sending, report generation, image processing — anything that shouldn't block the HTTP response.

bull-queue.tstypescript
// Register queue in module
@Module({
  imports: [
    BullModule.registerQueue({ name: "email" }),
  ],
  providers: [EmailProcessor],
})
export class EmailModule {}

// Producer — add jobs to queue
@Injectable()
export class OrderService {
  constructor(@InjectQueue("email") private emailQueue: Queue) {}

  async placeOrder(dto: CreateOrderDto) {
    const order = await this.orderRepo.save(dto);

    // Add job to queue — processed asynchronously
    await this.emailQueue.add("order-confirmation", {
      orderId: order.id,
      email: dto.email,
    }, {
      attempts: 3,                    // retry 3 times on failure
      backoff: { type: "exponential", delay: 2000 },
      removeOnComplete: true,
    });

    return order;
  }
}

// Consumer — processes jobs from queue
@Processor("email")
export class EmailProcessor {
  @Process("order-confirmation")
  async handleOrderConfirmation(job: Job<{ orderId: number; email: string }>) {
    const { orderId, email } = job.data;
    await this.emailService.sendOrderConfirmation(email, orderId);
  }

  @OnQueueFailed()
  handleFailure(job: Job, error: Error) {
    logger.error(`Job ${job.id} failed: ${error.message}`);
  }
}

Scaling Node.js

StrategyHowWhen
Cluster moduleFork workers per CPU coreSingle-server, utilize all cores
PM2Process manager with clustering, monitoring, auto-restartProduction single-server deployments
Docker + K8sContainerize, horizontal pod autoscalingCloud deployments, microservices
Worker ThreadsOffload CPU-heavy work to separate threadsImage processing, crypto, heavy computation
clustering.tstypescript
import cluster from "cluster";
import os from "os";

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Primary ${process.pid} forking ${numCPUs} workers`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on("exit", (worker) => {
    console.log(`Worker ${worker.process.pid} died. Restarting...`);
    cluster.fork(); // auto-restart crashed workers
  });
} else {
  // Each worker runs the full app
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  console.log(`Worker ${process.pid} started`);
}

// In production, use PM2 instead:
// pm2 start dist/main.js -i max  (auto-detect CPU count)

📝 Quick Revision

Quick Revision Cheat Sheet

Transports: TCP, Redis, NATS, RabbitMQ, Kafka, gRPC. Pick based on use case.

@MessagePattern: Request/response. Client sends, waits for reply.

@EventPattern: Fire and forget. No response. Decoupled.

Bull queue: Redis-backed job queue. Retries, backoff, concurrency. For background work.

Scaling: Cluster module or PM2 for multi-core. Docker + K8s for horizontal scaling.

12

Top 30 Interview Questions

These are the questions that actually get asked

Compiled from real interview experiences at product companies. Grouped by topic. Practice answering each in 2-3 minutes.

Node.js Internals (1-8)

Q:1. How does the Node.js event loop work? How is it different from the browser?

A: Node's event loop has 6 phases (timers, pending, idle, poll, check, close) powered by libuv. The browser's is simpler (task → microtasks → render). Node has process.nextTick (highest priority microtask) and setImmediate (check phase). Both drain microtasks between phases, but Node drains them between every phase, not just after macrotasks.

Q:2. What happens when you do CPU-intensive work in Node?

A: It blocks the event loop — no other requests can be processed until the computation finishes. Solutions: Worker Threads for parallel computation, child_process.fork for separate processes, or offload to a dedicated service. Never do heavy computation (image processing, crypto, large JSON parsing) on the main thread.

Q:3. Explain streams in Node.js. When would you use them?

A: Streams process data in chunks without loading everything into memory. Four types: Readable, Writable, Duplex, Transform. Use for large files, HTTP responses, real-time data. Always use pipeline() over .pipe() — it handles backpressure, errors, and cleanup automatically.

Q:4. What is backpressure and how do you handle it?

A: When a readable stream produces data faster than the writable can consume, data buffers in memory. Without handling, this can crash the process. pipeline() handles it automatically — pauses the readable when the writable's buffer is full, resumes when it drains. Manual handling: check write() return value, pause on false, resume on 'drain' event.

Q:5. CommonJS vs ES Modules — what are the differences?

A: CJS: require/module.exports, synchronous, runtime evaluation, no tree shaking. ESM: import/export, asynchronous, static analysis at compile time, tree-shakeable, supports top-level await. ESM is the standard going forward. Use 'type: module' in package.json to enable ESM for .js files.

Q:6. How does module caching work in Node.js?

A: When you require() a module, Node caches it in require.cache. Subsequent require() calls return the cached export — the module code runs only once. This is why singletons work naturally: export an instance, every importer gets the same object. ES Modules have the same behavior.

Q:7. What is the libuv thread pool and when is it used?

A: libuv provides a thread pool (default 4 threads, configurable via UV_THREADPOOL_SIZE) for operations that can't be done asynchronously at the OS level: file system operations, DNS lookups, and crypto. Network I/O (TCP, HTTP) uses OS-level async APIs (epoll/kqueue) and doesn't use the thread pool.

Q:8. How do you scale a Node.js application?

A: Cluster module (fork per CPU core), PM2 for process management with auto-restart, Docker + Kubernetes for horizontal scaling, and stateless design (sessions in Redis, not memory). For CPU-heavy work, use Worker Threads. In production, PM2 or K8s handle clustering, health checks, and zero-downtime deploys.

NestJS Architecture (9-16)

Q:9. What is NestJS and why use it over Express?

A: NestJS is an opinionated framework built on Express (or Fastify) that adds structure: modules, DI, decorators, guards, pipes, interceptors. Express is minimal — you build everything yourself. NestJS is like Spring Boot for Node: enforces architecture, provides testability via DI, and scales to large codebases. Use Express for small APIs, NestJS for production applications.

Q:10. Explain the NestJS request lifecycle.

A: Middleware → Guards → Interceptors (before) → Pipes → Route handler → Interceptors (after) → Exception filters. Each layer has a specific job: middleware for raw request processing, guards for auth, pipes for validation/transformation, interceptors for cross-cutting concerns, and exception filters for error formatting.

Q:11. How does dependency injection work in NestJS?

A: NestJS has a built-in IoC container. Classes decorated with @Injectable() are registered as providers. The container resolves the dependency graph and injects via constructor. Scopes: DEFAULT (singleton per module), REQUEST (per request), TRANSIENT (per injection). Custom providers use useFactory, useClass, or useValue for third-party integrations.

Q:12. What is the difference between Guards and Middleware?

A: Middleware runs first, has access to raw req/res, and doesn't know about NestJS execution context. Guards run after middleware, have access to ExecutionContext (which controller/handler will run), and return true/false to allow/deny. Use middleware for logging, CORS, body parsing. Use guards for authentication and authorization.

Q:13. How do Pipes work in NestJS?

A: Pipes run before the route handler. They transform input (string → number) or validate it (throw 400 if invalid). Built-in: ValidationPipe (class-validator), ParseIntPipe, ParseUUIDPipe. ValidationPipe with whitelist: true strips unknown properties, forbidNonWhitelisted: true rejects them. Apply globally, per controller, or per parameter.

Q:14. Explain Interceptors and give a use case.

A: Interceptors wrap the route handler execution — they run code before AND after. They receive an Observable of the response. Use cases: logging (measure duration), caching (return cached response), response transformation (wrap in { data: ... }), timeout handling. They're NestJS's equivalent of Spring AOP @Around advice.

Q:15. How do you handle errors globally in NestJS?

A: Create an exception filter with @Catch() decorator. Implement ExceptionFilter interface. Register globally with app.useGlobalFilters(). NestJS has built-in exceptions: NotFoundException (404), BadRequestException (400), UnauthorizedException (401), ForbiddenException (403). Custom exceptions extend HttpException.

Q:16. What are NestJS modules and how do they work?

A: Modules organize related code (controllers + providers). @Module() decorator defines imports (other modules), controllers, providers, and exports. Providers are scoped to their module — not accessible from other modules unless exported. The root AppModule imports all feature modules. Use forRoot/forRootAsync for dynamic module configuration.

Database & Auth (17-22)

Q:17. TypeORM vs Prisma — when would you pick each?

A: TypeORM: code-first with decorators, familiar if you know JPA/Hibernate, better NestJS integration (@nestjs/typeorm), QueryBuilder for complex queries. Prisma: schema-first, auto-generated types with perfect type safety, simpler API, better DX. Pick TypeORM for existing NestJS projects, Prisma for new projects where type safety is priority.

Q:18. How do you handle the N+1 problem in TypeORM?

A: Same as JPA: loading N entities then accessing lazy relations triggers N extra queries. Fix: use 'relations' option in find() to eager-load, or leftJoinAndSelect in QueryBuilder. In Prisma, use 'include' to specify which relations to load. Always check generated SQL with logging enabled.

Q:19. How do you implement JWT authentication in NestJS?

A: Use @nestjs/passport + @nestjs/jwt. Create a JwtStrategy extending PassportStrategy that extracts and validates the token. The validate() method returns the user payload (set as req.user). Create a JwtAuthGuard that uses the strategy. Apply with @UseGuards(JwtAuthGuard) on controllers or globally.

Q:20. How do you implement role-based access control in NestJS?

A: Create a @Roles() decorator using SetMetadata. Create a RolesGuard that reads the metadata via Reflector and checks req.user.role. Stack guards: @UseGuards(JwtAuthGuard, RolesGuard). The JWT guard authenticates, the roles guard authorizes. Create a @CurrentUser() decorator with createParamDecorator for clean user access.

Q:21. How do you handle database transactions in NestJS?

A: TypeORM: use QueryRunner for manual transaction control, or DataSource.transaction() for callback-based. Prisma: use prisma.$transaction() with an async callback — all operations inside share the same transaction. For NestJS-specific: use @nestjs/typeorm's EntityManager in a transactional service method.

Q:22. How do you handle database migrations?

A: TypeORM: typeorm migration:generate auto-generates from entity changes, migration:run applies, migration:revert rolls back. Prisma: prisma migrate dev generates from schema diff, migrate deploy for production. Always review generated migrations before applying. Never auto-sync in production (synchronize: false).

Advanced (23-30)

Q:23. How do you implement background jobs in NestJS?

A: Use Bull (Redis-backed queue) via @nestjs/bull. Define a queue in the module, inject it in the producer service to add jobs, create a @Processor class to consume jobs. Configure retries, backoff, concurrency, and dead letter handling. Use for email, reports, image processing — anything that shouldn't block HTTP responses.

Q:24. Explain NestJS microservices transport layers.

A: NestJS supports TCP, Redis, NATS, RabbitMQ, Kafka, and gRPC as transport layers. @MessagePattern for request/response (client sends, waits for reply). @EventPattern for fire-and-forget events. ClientProxy handles communication. Pick based on needs: Redis for simple pub/sub, RabbitMQ for reliability, Kafka for event streaming, gRPC for performance.

Q:25. How do you test NestJS applications?

A: Use @nestjs/testing's Test.createTestingModule to create isolated test modules with mocked dependencies. Unit tests: mock repos/services, test business logic. E2E tests: create full app with supertest, test HTTP flow. Mock with jest.fn() or provide mock implementations in the testing module. Use Testcontainers for real DB integration tests.

Q:26. What is the difference between @MessagePattern and @EventPattern?

A: @MessagePattern is request/response — the client sends a message and waits for a reply (like HTTP). @EventPattern is fire-and-forget — the client emits an event and doesn't wait. Use @MessagePattern when you need the result (get user data). Use @EventPattern for notifications, logging, analytics — where the sender doesn't care about the outcome.

Q:27. How do you handle configuration in NestJS?

A: Use @nestjs/config with ConfigModule.forRoot(). It loads .env files and provides ConfigService for type-safe access. Use envFilePath for different environments. Validate config with Joi or class-validator schemas. Make it global with isGlobal: true so every module can inject ConfigService without importing ConfigModule.

Q:28. How do you implement caching in NestJS?

A: Use @nestjs/cache-manager. Register CacheModule.register() with a store (memory, Redis). Use @CacheInterceptor on controllers for automatic HTTP response caching. For manual control, inject CACHE_MANAGER and use get/set/del. Use CacheTTL decorator to set per-route TTL. Redis store for distributed caching across instances.

Q:29. How do you handle WebSockets in NestJS?

A: Use @nestjs/websockets with @WebSocketGateway decorator. @SubscribeMessage for handling events. @WebSocketServer for accessing the server instance. NestJS supports Socket.IO and ws adapters. Guards, pipes, and interceptors work with WebSocket gateways too. Use for real-time features: chat, notifications, live updates.

Q:30. How do you deploy a NestJS application to production?

A: Build with nest build (compiles TypeScript). Run with node dist/main.js. Use PM2 for process management (clustering, auto-restart, log management). Dockerize with multi-stage build (build stage + slim runtime stage). Deploy to K8s with health checks (/health endpoint via @nestjs/terminus). Enable graceful shutdown with app.enableShutdownHooks().

13

Last Day Revision Sheet

📋 Scan this the night before your interview

This is the compressed version of everything above. If you can explain each line, you're ready.

Node.js Internals

Quick Revision Cheat Sheet

Event loop: 6 phases: timers → pending → idle → poll → check → close. Microtasks drain between phases.

nextTick vs setImmediate: nextTick: highest priority, before I/O. setImmediate: check phase, after I/O poll.

Thread pool: 4 threads (UV_THREADPOOL_SIZE). For fs, dns, crypto. Network uses OS async APIs.

V8 memory: Heap (objects) + stack (calls). Generational GC. ~1.5GB default limit.

Blocking trap: CPU work blocks the loop. Use Worker Threads for heavy computation.

Streams & Modules

Quick Revision Cheat Sheet

Streams: Readable, Writable, Duplex, Transform. Process data in chunks, constant memory.

pipeline(): Use over .pipe(). Handles backpressure, errors, cleanup automatically.

CJS vs ESM: CJS: require, sync, runtime. ESM: import, async, static, tree-shakeable.

Module cache: Modules run once, cached. Singletons work naturally.

Error Handling & Express

Quick Revision Cheat Sheet

Async errors: Always try/catch with async/await. Always .catch() on promises.

Operational vs programmer: Operational: handle gracefully. Programmer: crash and restart.

Express middleware: (req, res, next) — call next() or respond. Order matters. Error middleware has 4 params.

Fastify: 5x faster than Express. Built-in validation and logging. NestJS supports both.

NestJS Architecture

Quick Revision Cheat Sheet

Building blocks: Module, Controller, Provider, Guard, Pipe, Interceptor, Exception Filter.

Request lifecycle: Middleware → Guard → Interceptor (before) → Pipe → Handler → Interceptor (after) → Filter.

DI: @Injectable + constructor injection. Scopes: DEFAULT (singleton), REQUEST, TRANSIENT.

Modules: Group controllers + providers. Export to share. forRoot/forRootAsync for dynamic config.

Pipes, Guards & Interceptors

Quick Revision Cheat Sheet

Pipes: Validate + transform input. ValidationPipe with class-validator DTOs. whitelist: true.

Guards: Auth + roles. CanActivate interface. Return true/false. Run before pipes.

Interceptors: Before + after handler. Logging, caching, response wrapping. Observable-based.

Exception filters: @Catch() for global error handling. Format error responses consistently.

Database & Auth

Quick Revision Cheat Sheet

TypeORM: Code-first, decorators. QueryBuilder for complex queries. @nestjs/typeorm integration.

Prisma: Schema-first. Auto-generated types. Fluent API. Better type safety.

N+1 fix: TypeORM: relations or leftJoinAndSelect. Prisma: include.

JWT auth: Passport + JwtStrategy. validate() sets req.user. JwtAuthGuard + RolesGuard.

Refresh tokens: Short access (15m) + long refresh (7d). Store refresh server-side.

Testing & Microservices

Quick Revision Cheat Sheet

Unit tests: Test.createTestingModule + mock providers. Jest + jest.fn().

E2E tests: Supertest + full app. Test HTTP flow, validation, auth.

@MessagePattern: Request/response. Client sends, waits for reply.

@EventPattern: Fire and forget. Decoupled. No response.

Bull queue: Redis-backed jobs. Retries, backoff, concurrency. For background work.

Production

Quick Revision Cheat Sheet

Clustering: Cluster module or PM2 -i max. One worker per CPU core.

Docker: Multi-stage build. Slim runtime image. node dist/main.js.

Health checks: @nestjs/terminus. /health endpoint. Check DB, Redis, external services.

Graceful shutdown: app.enableShutdownHooks(). Finish in-flight requests before stopping.

Config: @nestjs/config + ConfigService. Validate with Joi. isGlobal: true.