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
- Timers: Executes callbacks scheduled by
setTimeout()andsetInterval(). - Pending Callbacks: Executes I/O callbacks deferred to the next loop iteration (e.g., TCP errors).
- Idle, Prepare: Used internally only.
- 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. - Check: Executes
setImmediate()callbacks. - 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:
- Next Tick Queue:
process.nextTick(). Highest priority, runs before any other microtasks. - 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.promisifyto 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.exports | import and export |
| Synchronous loading | Asynchronous loading |
| Dynamic (can require conditionally) | Static (imports must be top-level) |
| Default in Node.js historically | Standard 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, orclass-validator(NestJS). Never trustreq.bodydirectly. - SQL/NoSQL Injection: Use parameterized queries (
$1placeholders) or ORMs (Prisma, TypeORM). Never concatenate user input into queries. - Rate Limiting: Use
express-rate-limitor NestJS's@nestjs/throttlerto 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 auditregularly. Usenpm audit fixfor patching. Consider Snyk for CI/CD. - Environment Variables: Never hardcode secrets. Use
.envfiles locally and secret managers (Vault, AWS Secrets Manager) in production.
Performance & Scaling
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.jsPerformance 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:
- Middleware: Standard Express/Fastify middleware.
- Guards: Determine if the request should be handled (Authentication/Authorization).
@UseGuards(JwtAuthGuard) - Interceptors (pre-controller): Transform the request or bind extra logic before execution.
- Pipes: Validate and transform request payloads.
@UsePipes(ValidationPipe) - Controller Handler: The actual route method.
- Interceptors (post-controller): Transform the response.
- 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
| Framework | Architecture | Strengths | Weaknesses |
|---|---|---|---|
| Express | Unopinionated, minimal | Huge ecosystem, simple learning curve, industry standard | Callback-heavy middleware, lacks structure for large apps |
| Fastify | Schema-driven, highly optimized | Extremely fast, built-in JSON schema validation, async-await native | Smaller ecosystem than Express |
| NestJS | Opinionated, Angular-like | Excellent for enterprise, strict TypeScript, built-in DI architecture | Steep 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
| Topic | Key Points to Mention |
|---|---|
| Event Loop | Single-threaded, non-blocking, phases (timers, poll, check), nextTick vs setImmediate |
| Async Patterns | Promise.all vs Promise.allSettled, async/await, try/catch |
| Error Handling | Custom error classes, operational vs programmer errors, process.on('unhandledRejection'), graceful shutdown |
| Memory | V8 heap limits (~1.4GB), buffer, streams for large data to prevent OOM |
| Scaling | Cluster (multi-process) vs Worker Threads (multi-thread for CPU bound tasks) |
| Security | Input validation (zod/joi), helmet, rate limiting, parameterized queries, CORS |
| NestJS DI | IoC container, singletons by default, easy mocking for tests, modules encapsulate scope |
| NestJS Lifecycle | Middleware → Guards → Interceptors → Pipes → Controller → Filters |
| NestJS Advanced | Custom guards/decorators, interceptors for logging/transform, microservice transports (TCP, Redis, Kafka) |