Interview Prep Hub

Node.js & NestJS

You used Node.js to containerize microservices for financial calculators and scoring engines. Expect questions on the event loop, async patterns, and Nest's DI system.

The Event Loop — Deep Dive

Rendering diagram…

Node is single-threaded but non-blocking. The event loop is what allows Node.js to perform non-blocking I/O operations by offloading operations to the system kernel whenever possible. It runs in phases.

Phases of the Event Loop

  1. Timers: Executes callbacks scheduled by setTimeout() and setInterval().
  2. Pending Callbacks: Executes I/O callbacks deferred to the next loop iteration (e.g., TCP errors).
  3. Idle, Prepare: Used internally only.
  4. Poll: Retrieve new I/O events; execute I/O related callbacks. If the poll queue is empty, it will block here until timers expire or setImmediate() is scheduled.
  5. Check: Executes setImmediate() callbacks.
  6. Close Callbacks: Executes close events, e.g., socket.on('close', ...).

Microtasks vs Macrotasks

Microtasks run between every phase of the event loop. There are two microtask queues:

  1. Next Tick Queue: process.nextTick(). Highest priority, runs before any other microtasks.
  2. Promise Queue: Promise.resolve().then(). Runs after next ticks but before the event loop continues.
console.log('1');
setTimeout(() => console.log('2 (Timer)'), 0);
setImmediate(() => console.log('3 (Check)'));
process.nextTick(() => console.log('4 (NextTick)'));
Promise.resolve().then(() => console.log('5 (Promise)'));
console.log('6');

// Output order: 
// 1
// 6
// 4 (NextTick)
// 5 (Promise)
// 2 (Timer) or 3 (Check) — order of these two can vary depending on system performance if called in main module.

Async Patterns

  • Callbacks — legacy, prone to "callback hell" or "pyramid of doom". Use util.promisify to wrap them.
  • Promises.then()/.catch(), chainable state machines (Pending, Fulfilled, Rejected).
  • async/await — syntactic sugar over Promises, always prefer for new code. Makes async code look synchronous.
// Run in parallel, fail fast if any errors
const [user, orders] = await Promise.all([
  getUser(id),
  getOrders(id)
]);

// Run all, get individual results even on failure
const results = await Promise.allSettled([task1(), task2()]);
// results[0].status === 'fulfilled' || 'rejected'

// Race — first to finish wins (good for timeouts)
const fastest = await Promise.race([
  fetchData(), 
  new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
]);

// Any - first to fulfill wins (ignores rejections unless all reject)
const firstSuccess = await Promise.any([ping(server1), ping(server2)]);

Error Handling Patterns

Proper error handling is critical in Node.js — an unhandled error can crash your entire process.

Async Error Handling

// Always wrap async/await in try-catch
async function getUser(id) {
  try {
    const user = await db.findUser(id);
    if (!user) throw new NotFoundError('User not found');
    return user;
  } catch (err) {
    if (err instanceof NotFoundError) throw err;
    throw new InternalError('Failed to fetch user', { cause: err });
  }
}

// Custom error classes (essential for APIs)
class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational; // operational vs programmer error
    Error.captureStackTrace(this, this.constructor);
  }
}

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

class ValidationError extends AppError {
  constructor(errors) {
    super('Validation failed', 400);
    this.errors = errors;
  }
}

// Global error handler middleware (Express)
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  const message = err.isOperational ? err.message : 'Internal server error';
  
  // Log programmer errors with full stack
  if (!err.isOperational) {
    console.error('UNEXPECTED ERROR:', err);
  }
  
  res.status(status).json({ error: message });
});

Process-Level Error Handling

// Catch unhandled promise rejections (CRITICAL)
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // Log, send to Sentry, then gracefully shut down
  process.exit(1);
});

// Catch uncaught exceptions
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  // Must exit — process state is unreliable after uncaught exception
  process.exit(1);
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('SIGTERM received. Shutting down gracefully...');
  await server.close();     // stop accepting new connections
  await db.disconnect();    // close DB pool
  process.exit(0);
});

Core Modules & Concepts

Streams & Buffers

Buffers are raw memory allocations outside the V8 heap, used to process binary data. Streams are abstract interfaces for working with streaming data. They prevent memory exhaustion when reading large files.

  • Readable: fs.createReadStream
  • Writable: fs.createWriteStream
  • Duplex: Both readable and writable (TCP sockets)
  • Transform: Duplex stream that modifies data as it is read/written (zlib.createGzip)
// Efficient file copy without loading whole file into memory
const { pipeline } = require('stream/promises');
const fs = require('fs');
const zlib = require('zlib');

await pipeline(
  fs.createReadStream('input.txt'),
  zlib.createGzip(),
  fs.createWriteStream('input.txt.gz')
);

// Transform stream example — process CSV line-by-line
const { Transform } = require('stream');

class CSVParser extends Transform {
  _transform(chunk, encoding, callback) {
    const lines = chunk.toString().split('\n');
    for (const line of lines) {
      const record = line.split(',');
      this.push(JSON.stringify(record) + '\n');
    }
    callback();
  }
}

fs.createReadStream('data.csv')
  .pipe(new CSVParser())
  .pipe(fs.createWriteStream('output.json'));

EventEmitter

The core of Node's event-driven architecture. Much of the Node.js core API is built around an idiomatic asynchronous event-driven architecture.

const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', (data) => {
  console.log('an event occurred!', data);
});
myEmitter.emit('event', { id: 1 });

// Real-world pattern: Order processing
class OrderProcessor extends EventEmitter {
  async processOrder(order) {
    try {
      await this.chargePayment(order);
      this.emit('payment:success', order);
      
      await this.fulfillOrder(order);
      this.emit('order:fulfilled', order);
    } catch (err) {
      this.emit('order:failed', order, err);
    }
  }
}

const processor = new OrderProcessor();
processor.on('payment:success', (order) => sendReceipt(order));
processor.on('order:fulfilled', (order) => notifyWarehouse(order));
processor.on('order:failed', (order, err) => alertOps(order, err));

CommonJS vs ES Modules

CommonJS (CJS)ES Modules (ESM)
require() and module.exportsimport and export
Synchronous loadingAsynchronous loading
Dynamic (can require conditionally)Static (imports must be top-level)
Default in Node.js historicallyStandard in browsers, native in Node.js via .mjs or "type": "module"

Security Best Practices

  • Input Validation: Always validate and sanitize inputs. Use libraries like joi, zod, or class-validator (NestJS). Never trust req.body directly.
  • SQL/NoSQL Injection: Use parameterized queries ($1 placeholders) or ORMs (Prisma, TypeORM). Never concatenate user input into queries.
  • Rate Limiting: Use express-rate-limit or NestJS's @nestjs/throttler to prevent brute force attacks.
  • Helmet: app.use(helmet()) sets security HTTP headers (CSP, X-Frame-Options, etc.).
  • CORS: Be explicit about allowed origins. Never use cors({ origin: '*' }) in production with credentials.
  • Dependency Auditing: Run npm audit regularly. Use npm audit fix for patching. Consider Snyk for CI/CD.
  • Environment Variables: Never hardcode secrets. Use .env files locally and secret managers (Vault, AWS Secrets Manager) in production.

Performance & Scaling

CPU-bound work on the event loop JSON.parse of 50MB / bcrypt / image resize req every other request waits — throughput collapses, health checks time out offloaded requests keep flowing worker_threads / child process / job queue worker_threads for CPU work, a queue + worker for long jobs, cluster / PM2 for all cores. async/await does NOT help here — it only yields on I/O, and this work never waits on I/O.
One thread runs your JavaScript. A CPU-bound function blocks every other request on that process — the answer is a worker, not more async.

Worker Threads vs Child Processes vs Cluster

  • Cluster: Spawns multiple Node.js processes that share the same port. Good for scaling web servers across CPU cores. Each process has its own memory and V8 instance.
  • Child Processes: spawn, exec, fork. Spawns an entirely new process. Good for running external scripts or binaries.
  • Worker Threads: Threads within the same process. They share memory (via SharedArrayBuffer). Ideal for CPU-intensive JavaScript tasks (crypto, image processing) without blocking the event loop.

Memory Limits

If your app crashes with FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory, you can increase it via:

node --max-old-space-size=4096 index.js

Performance Monitoring

// Built-in performance hooks
const { performance, PerformanceObserver } = require('perf_hooks');

// Measure operation duration
performance.mark('start-db-query');
const result = await db.query('SELECT * FROM users');
performance.mark('end-db-query');
performance.measure('DB Query', 'start-db-query', 'end-db-query');

// Observe measurements
const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration}ms`);
  });
});
obs.observe({ entryTypes: ['measure'] });

// Memory monitoring
const used = process.memoryUsage();
console.log({
  rss: `${Math.round(used.rss / 1024 / 1024)} MB`,      // Total memory
  heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`, // V8 heap
  heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`,  // Used heap
  external: `${Math.round(used.external / 1024 / 1024)} MB`,   // C++ objects bound to JS
});

NestJS Core Concepts

NestJS is to Node what Spring is to Java — opinionated framework with modules, controllers, providers, decorators, and dependency injection. Built on Express or Fastify.

Modules, Controllers, Providers

@Module({
  imports: [DatabaseModule],
  controllers: [CalculatorController],
  providers: [CalculatorService, ScoringEngine],
  exports: [CalculatorService],
})
export class CalculatorModule {}

@Injectable()
export class CalculatorService {
  constructor(@Inject('DB') private db: Database) {}
  async computeEMI(principal: number, rate: number, months: number) {
    // business logic
  }
}

@Controller('calc')
export class CalculatorController {
  constructor(private svc: CalculatorService) {}
  @Get('emi')
  emi(@Query('p') p: number, @Query('r') r: number, @Query('n') n: number) {
    return this.svc.computeEMI(p, r, n);
  }
}

The Request Lifecycle (Pipes, Guards, Interceptors)

Order of execution for an incoming request:

  1. Middleware: Standard Express/Fastify middleware.
  2. Guards: Determine if the request should be handled (Authentication/Authorization). @UseGuards(JwtAuthGuard)
  3. Interceptors (pre-controller): Transform the request or bind extra logic before execution.
  4. Pipes: Validate and transform request payloads. @UsePipes(ValidationPipe)
  5. Controller Handler: The actual route method.
  6. Interceptors (post-controller): Transform the response.
  7. Exception Filters: Catch errors and format responses globally.

Dependency Injection (DI)

Nest creates an IoC (Inversion of Control) container. When a class asks for a dependency in its constructor, Nest instantiates it (as a singleton by default) and injects it. This makes testing trivial by injecting mocks.

// Custom Provider example
@Module({
  providers: [
    {
      provide: 'API_KEY',
      useValue: 'secret-key-123', // Can inject values, factories, or classes
    }
  ]
})

Advanced NestJS Patterns

Custom Guards

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) return true;
    
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

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

// Usage
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
  @Get('users')
  @Roles('admin', 'super-admin')
  findAllUsers() { /* ... */ }
}

Custom Interceptors

// Logging interceptor — measures request duration
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const now = Date.now();
    
    return next.handle().pipe(
      tap(() => {
        console.log(`${req.method} ${req.url} — ${Date.now() - now}ms`);
      }),
    );
  }
}

// Transform response interceptor
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

Exception Filters

@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,
      message: exception.message,
    });
  }
}

NestJS Microservices

// Transport layers — NestJS supports multiple patterns
// TCP, Redis, NATS, MQTT, Kafka, gRPC, RabbitMQ

// Microservice server (TCP example)
const app = await NestFactory.createMicroservice(AppModule, {
  transport: Transport.TCP,
  options: { host: '0.0.0.0', port: 3001 },
});

// Message pattern handler
@Controller()
export class MathController {
  @MessagePattern({ cmd: 'sum' })
  accumulate(data: number[]): number {
    return data.reduce((a, b) => a + b, 0);
  }
  
  @EventPattern('user_created')
  async handleUserCreated(data: { userId: string }) {
    // Event-based: fire-and-forget (no response expected)
    await this.analyticsService.track('user_created', data);
  }
}

// Client calling the microservice
@Injectable()
export class AppService {
  constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {}
  
  async getSum(numbers: number[]) {
    return this.client.send({ cmd: 'sum' }, numbers).toPromise();
  }
}

Express vs Fastify vs NestJS

FrameworkArchitectureStrengthsWeaknesses
ExpressUnopinionated, minimalHuge ecosystem, simple learning curve, industry standardCallback-heavy middleware, lacks structure for large apps
FastifySchema-driven, highly optimizedExtremely fast, built-in JSON schema validation, async-await nativeSmaller ecosystem than Express
NestJSOpinionated, Angular-likeExcellent for enterprise, strict TypeScript, built-in DI architectureSteep learning curve, lots of boilerplate

TypeScript Essentials for Node.js

NestJS is TypeScript-first. Interviewers expect you to know these TypeScript patterns.

// Utility types you must know
type Partial<T>    // Makes all properties optional
type Required<T>   // Makes all properties required
type Pick<T, K>    // Pick specific properties
type Omit<T, K>    // Remove specific properties
type Record<K, V>  // Key-value map type

// Practical examples
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

type CreateUserDTO = Omit<User, 'id'>;           // No ID on creation
type UpdateUserDTO = Partial<Omit<User, 'id'>>;   // All fields optional for update
type UserSummary = Pick<User, 'id' | 'name'>;     // Just id and name

// Generics in service patterns
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(data: Partial<T>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> { /* ... */ }
  // ... other methods
}

Interview Quick Reference

TopicKey Points to Mention
Event LoopSingle-threaded, non-blocking, phases (timers, poll, check), nextTick vs setImmediate
Async PatternsPromise.all vs Promise.allSettled, async/await, try/catch
Error HandlingCustom error classes, operational vs programmer errors, process.on('unhandledRejection'), graceful shutdown
MemoryV8 heap limits (~1.4GB), buffer, streams for large data to prevent OOM
ScalingCluster (multi-process) vs Worker Threads (multi-thread for CPU bound tasks)
SecurityInput validation (zod/joi), helmet, rate limiting, parameterized queries, CORS
NestJS DIIoC container, singletons by default, easy mocking for tests, modules encapsulate scope
NestJS LifecycleMiddleware → Guards → Interceptors → Pipes → Controller → Filters
NestJS AdvancedCustom guards/decorators, interceptors for logging/transform, microservice transports (TCP, Redis, Kafka)