Pytania na rozmowie kwalifikacyjnej z Node.js Backend: Kompletny przewodnik 2026

25 najczęściej zadawanych pytań na rozmowie kwalifikacyjnej z Node.js backend. Event loop, async/await, strumienie, klasteryzacja i wydajność z szczegółowymi odpowiedziami.

Pytania na rozmowie kwalifikacyjnej z Node.js Backend - Kompletny przewodnik

Rozmowy kwalifikacyjne z Node.js backend weryfikują zrozumienie wewnętrznych mechanizmów środowiska uruchomieniowego, biegłość w asynchronicznych wzorcach programowania oraz zdolność do projektowania wydajnych aplikacji. Niniejszy przewodnik obejmuje najczęściej zadawane pytania, od podstaw po zaawansowane koncepcje produkcyjne.

Wskazówka rekrutacyjna

Rekruterzy cenią odpowiedzi łączące teorię z praktycznymi przykładami. Przy każdym pytaniu warto zilustrować odpowiedź kodem lub konkretnym przypadkiem użycia, co świadczy o rzeczywistym doświadczeniu.

Podstawy Node.js

Pytanie 1: Czym jest Event Loop i jak działa?

Event Loop to podstawowy mechanizm pozwalający Node.js obsługiwać operacje asynchroniczne w sposób nieblokujący, mimo działania na jednym wątku. Koordynuje wykonywanie kodu JavaScript, callbacków i zdarzeń.

event-loop-demo.jsjavascript
// Demonstration of Event Loop execution order

console.log('1. Script start (synchronous)');

// setTimeout goes to the Timer Queue
setTimeout(() => {
  console.log('5. setTimeout callback (Timer Queue)');
}, 0);

// setImmediate goes to the Check Queue
setImmediate(() => {
  console.log('6. setImmediate callback (Check Queue)');
});

// Promise goes to the Microtask Queue (priority)
Promise.resolve().then(() => {
  console.log('3. Promise.then (Microtask Queue)');
});

// process.nextTick has the highest priority
process.nextTick(() => {
  console.log('2. process.nextTick (nextTick Queue)');
});

console.log('4. Script end (synchronous)');

// Output order: 1, 4, 2, 3, 5, 6

Event Loop przetwarza kolejki w ściśle określonej kolejności: najpierw kod synchroniczny, następnie nextTick, mikrozadania (Promise), timery, callbacki I/O, setImmediate, a na końcu callbacki zamknięcia.

Pytanie 2: Jaka jest różnica między process.nextTick() a setImmediate()?

To pytanie sprawdza szczegółowe zrozumienie priorytetów wykonywania w Event Loop.

nextTick-vs-immediate.jsjavascript
// Behavior comparison

// process.nextTick executes BEFORE the next Event Loop phase
process.nextTick(() => {
  console.log('nextTick 1');
  process.nextTick(() => {
    console.log('nextTick 2 (nested)');
  });
});

// setImmediate executes in the Check phase of the Event Loop
setImmediate(() => {
  console.log('setImmediate 1');
  setImmediate(() => {
    console.log('setImmediate 2 (nested)');
  });
});

// Output: nextTick 1, nextTick 2, setImmediate 1, setImmediate 2

process.nextTick() jest przetwarzany natychmiast po bieżącej operacji, zanim Event Loop przejdzie dalej. Nadmierne użycie może zablokować Event Loop. setImmediate() jest bardziej przewidywalny i zalecany do odraczania wykonywania.

Uwaga na zagłodzenie

Rekurencyjne wywołania process.nextTick() mogą zagłodzić Event Loop i uniemożliwić przetwarzanie operacji I/O. Dla niekrytycznych operacji lepiej stosować setImmediate().

Pytanie 3: Jak Node.js obsługuje błędy w kodzie asynchronicznym?

Obsługa błędów asynchronicznych różni się zasadniczo od kodu synchronicznego. Bez odpowiedniej obsługi błąd może spowodować awarię całej aplikacji.

error-handling.jsjavascript
// Asynchronous error handling patterns

// Pattern 1: Callbacks with error-first convention
function readFileCallback(path, callback) {
  const fs = require('fs');
  fs.readFile(path, 'utf8', (err, data) => {
    if (err) {
      // Error is ALWAYS the first argument
      return callback(err, null);
    }
    callback(null, data);
  });
}

// Pattern 2: Promises with catch
async function readFilePromise(path) {
  const fs = require('fs').promises;
  try {
    const data = await fs.readFile(path, 'utf8');
    return data;
  } catch (err) {
    // Centralized error handling
    console.error(`File read error: ${err.message}`);
    throw err; // Re-throw for propagation
  }
}

// Pattern 3: Global handling of unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection:', reason);
  // In production: log and graceful shutdown
});

// Pattern 4: Handling uncaught exceptions
process.on('uncaughtException', (err) => {
  console.error('Uncaught exception:', err);
  // CRITICAL: always terminate the process after
  process.exit(1);
});

W środowisku produkcyjnym każdy Promise musi posiadać .catch() lub znajdować się wewnątrz bloku try/catch. Globalne handlery stanowią siatkę bezpieczeństwa, a nie główne rozwiązanie.

Programowanie asynchroniczne i współbieżność

Pytanie 4: Wyjaśnij różnicę między równoległością a współbieżnością w Node.js

Node.js jest współbieżny, ale domyślnie nie równoległy. To rozróżnienie jest fundamentalne dla zrozumienia wydajności.

concurrency-vs-parallelism.jsjavascript
// CONCURRENCY: multiple tasks progress by alternating (single-thread)
async function concurrentTasks() {
  console.time('concurrent');

  // These calls are concurrent, not parallel
  const results = await Promise.all([
    fetch('https://api.example.com/users'),    // Non-blocking I/O
    fetch('https://api.example.com/products'), // Non-blocking I/O
    fetch('https://api.example.com/orders'),   // Non-blocking I/O
  ]);

  console.timeEnd('concurrent'); // ~time of the longest request
  return results;
}

// PARALLELISM: with Worker Threads for CPU-bound tasks
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  // Main thread delegates CPU-intensive work
  async function parallelComputation() {
    console.time('parallel');

    const workers = [
      createWorker({ start: 0, end: 1000000 }),
      createWorker({ start: 1000000, end: 2000000 }),
      createWorker({ start: 2000000, end: 3000000 }),
    ];

    const results = await Promise.all(workers);
    console.timeEnd('parallel');
    return results.reduce((a, b) => a + b, 0);
  }

  function createWorker(data) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, { workerData: data });
      worker.on('message', resolve);
      worker.on('error', reject);
    });
  }
} else {
  // Code executed in the Worker Thread
  const { workerData } = require('worker_threads');
  let sum = 0;
  for (let i = workerData.start; i < workerData.end; i++) {
    sum += Math.sqrt(i); // CPU-intensive calculation
  }
  parentPort.postMessage(sum);
}

Dla operacji I/O (sieć, pliki) natywna współbieżność jest wystarczająca. Dla zadań obciążających procesor (intensywne obliczenia, kryptografia) Worker Threads umożliwiają prawdziwą równoległość.

Pytanie 5: Jak działa moduł Cluster?

Moduł Cluster pozwala tworzyć wiele procesów Node.js współdzielących ten sam port, wykorzystując w ten sposób wszystkie dostępne rdzenie procesora.

cluster-example.jsjavascript
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
  console.log(`Forking ${numCPUs} workers...`);

  // Fork one worker per CPU core
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // Handle crashing workers
  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (${signal || code})`);
    console.log('Starting a new worker...');
    cluster.fork(); // Automatic restart
  });

  // Inter-process communication
  cluster.on('message', (worker, message) => {
    console.log(`Message from worker ${worker.id}:`, message);
  });

} else {
  // Workers share the TCP port
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}\n`);

    // Send stats to primary
    process.send({ type: 'request', pid: process.pid });
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

Równowaga obciążenia jest obsługiwana automatycznie przez system operacyjny (round-robin na Linux/macOS). W produkcji PM2 upraszcza zarządzanie dzięki wbudowanemu trybowi klastra.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Strumienie i bufory

Pytanie 6: Kiedy należy używać strumieni zamiast klasycznych metod?

Strumienie umożliwiają przetwarzanie danych we fragmentach zamiast ładowania wszystkiego do pamięci. Są niezbędne przy dużych plikach i scenariuszach strumieniowania.

streams-comparison.jsjavascript
const fs = require('fs');

// ❌ BAD: loads entire file into memory
async function readEntireFile(path) {
  const data = await fs.promises.readFile(path); // Blocks if file > RAM
  return processData(data);
}

// ✅ GOOD: chunk-based processing with Stream
function readWithStream(path) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    const readStream = fs.createReadStream(path, {
      highWaterMark: 64 * 1024, // 64KB per chunk
    });

    readStream.on('data', (chunk) => {
      // Progressive processing, constant memory
      chunks.push(processChunk(chunk));
    });

    readStream.on('end', () => resolve(chunks));
    readStream.on('error', reject);
  });
}

// ✅ BEST: pipeline for chaining transformations
const { pipeline } = require('stream/promises');
const zlib = require('zlib');

async function compressFile(input, output) {
  await pipeline(
    fs.createReadStream(input),   // Source
    zlib.createGzip(),            // Transform
    fs.createWriteStream(output)  // Destination
  );
  // Automatic error handling and backpressure management
}

Strumienie należy stosować zawsze, gdy rozmiar danych może przekroczyć kilka MB lub gdy potrzebne jest przetwarzanie w czasie rzeczywistym (uploady, logi, dane sieciowe).

Pytanie 7: Wyjaśnij pojęcie backpressure

Backpressure występuje, gdy producent danych jest szybszy niż konsument. Bez odpowiedniego zarządzania pamięć szybko się wyczerpuje.

backpressure-demo.jsjavascript
const fs = require('fs');

// ❌ Problem: no backpressure handling
function badCopy(src, dest) {
  const readable = fs.createReadStream(src);
  const writable = fs.createWriteStream(dest);

  readable.on('data', (chunk) => {
    // If write() returns false, the internal buffer is full
    // But here reading continues anyway → memory leak
    writable.write(chunk);
  });
}

// ✅ Solution: respect the writable signal
function goodCopy(src, dest) {
  const readable = fs.createReadStream(src);
  const writable = fs.createWriteStream(dest);

  readable.on('data', (chunk) => {
    const canContinue = writable.write(chunk);

    if (!canContinue) {
      // Pause reading until buffer drains
      readable.pause();
    }
  });

  writable.on('drain', () => {
    // Buffer drained, resume reading
    readable.resume();
  });

  readable.on('end', () => writable.end());
}

// ✅ BEST: pipe() handles everything automatically
function bestCopy(src, dest) {
  const readable = fs.createReadStream(src);
  const writable = fs.createWriteStream(dest);

  // pipe() handles backpressure natively
  readable.pipe(writable);
}

Metoda pipe() lub pipeline() obsługuje backpressure automatycznie. W przypadku złożonych scenariuszy należy zaimplementować logikę pause/resume ręcznie.

Wydajność i optymalizacja

Pytanie 8: Jak zidentyfikować i naprawić wycieki pamięci?

Wycieki pamięci są częstym problemem w Node.js. Umiejętność ich wykrywania i naprawiania jest niezbędna w środowisku produkcyjnym.

memory-leak-patterns.jsjavascript
// ❌ Leak 1: closures that retain references
function createLeakyHandler() {
  const hugeData = Buffer.alloc(100 * 1024 * 1024); // 100MB

  return function handler(req, res) {
    // hugeData remains in memory as long as handler exists
    res.end('Hello');
  };
}

// ✅ Fix: limit the scope
function createSafeHandler() {
  return function handler(req, res) {
    // Data created and released on each request
    const data = fetchData();
    res.end(data);
  };
}

// ❌ Leak 2: event listeners not cleaned up
class LeakyClass {
  constructor() {
    // Added on each instantiation, never removed
    process.on('message', this.handleMessage);
  }
  handleMessage(msg) { /* ... */ }
}

// ✅ Fix: explicit cleanup
class SafeClass {
  constructor() {
    this.boundHandler = this.handleMessage.bind(this);
    process.on('message', this.boundHandler);
  }
  handleMessage(msg) { /* ... */ }

  destroy() {
    // Mandatory cleanup
    process.removeListener('message', this.boundHandler);
  }
}

// Diagnostics with native tools
function diagnoseMemory() {
  const used = process.memoryUsage();
  console.log({
    heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
    external: `${Math.round(used.external / 1024 / 1024)}MB`,
    rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
  });
}

// Enable manual garbage collector for testing
// node --expose-gc app.js
if (global.gc) {
  global.gc();
  diagnoseMemory();
}

W produkcji warto korzystać z narzędzi takich jak clinic.js, zrzutów sterty z Chrome DevTools lub rozwiązań APM (Application Performance Monitoring), np. DataDog czy New Relic.

Pytanie 9: Jak zoptymalizować wydajność API Node.js?

To pytanie weryfikuje znajomość technik optymalizacji na wielu poziomach.

performance-optimization.jsjavascript
// 1. CACHING: reduce expensive calls
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5-minute TTL

async function getCachedUser(id) {
  const cacheKey = `user:${id}`;
  let user = cache.get(cacheKey);

  if (!user) {
    user = await db.users.findById(id);
    cache.set(cacheKey, user);
  }

  return user;
}

// 2. CONNECTION POOLING: reuse DB connections
const { Pool } = require('pg');
const pool = new Pool({
  max: 20,                // Max simultaneous connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// 3. COMPRESSION: reduce response size
const compression = require('compression');
app.use(compression({
  filter: (req, res) => {
    // Only compress if > 1KB
    return compression.filter(req, res);
  },
  threshold: 1024,
}));

// 4. BATCHING: group operations
async function batchInsert(items) {
  const BATCH_SIZE = 1000;

  for (let i = 0; i < items.length; i += BATCH_SIZE) {
    const batch = items.slice(i, i + BATCH_SIZE);
    await db.items.insertMany(batch);
  }
}

// 5. LAZY LOADING: load on demand
async function getUserWithPosts(userId, includePosts = false) {
  const user = await db.users.findById(userId);

  if (includePosts) {
    user.posts = await db.posts.findByUserId(userId);
  }

  return user;
}

Optymalizacje powinny być oparte na profilowaniu. Przed optymalizacją warto zmierzyć rzeczywiste wąskie gardła.

Zasada 80/20

80% problemów wydajnościowych pochodzi z 20% kodu. Profilowanie pozwala zidentyfikować te krytyczne obszary zanim zacznie się ślepo optymalizować.

Bezpieczeństwo

Pytanie 10: Jak zabezpieczyć API Node.js przed typowymi atakami?

Bezpieczeństwo to stały temat na rozmowach kwalifikacyjnych. Oczekuje się wykazania znajomości podatności OWASP.

security-best-practices.jsjavascript
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');

const app = express();

// 1. SECURITY HEADERS with Helmet
app.use(helmet());

// 2. RATE LIMITING against brute-force attacks
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requests per IP
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
});
app.use('/api/', limiter);

// 3. SANITIZATION against NoSQL injections
app.use(mongoSanitize());

// 4. XSS PROTECTION
app.use(xss());

// 5. STRICT INPUT VALIDATION
const { body, validationResult } = require('express-validator');

app.post('/api/users',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }).escape(),
    body('name').trim().escape(),
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Continue processing
  }
);

// 6. SQL INJECTION PROTECTION (with parameters)
async function safeQuery(userId) {
  // ✅ Parameterized query
  const result = await pool.query(
    'SELECT * FROM users WHERE id = $1',
    [userId]
  );
  return result.rows;
}

// ❌ NEVER string concatenation
async function unsafeQuery(userId) {
  // Vulnerable to SQL injection
  const result = await pool.query(
    `SELECT * FROM users WHERE id = ${userId}`
  );
}

W produkcji należy również dodać: restrykcyjne CORS, obowiązkowe HTTPS, logowanie bezpieczeństwa, rotację sekretów i regularne audyty zależności (npm audit).

Architektura i wzorce projektowe

Pytanie 11: Wyjaśnij wzorzec Repository w Node.js

Wzorzec Repository abstrahuje dostęp do danych i ułatwia testowanie oraz utrzymanie kodu.

repository-pattern.jsjavascript
// Abstract interface (for TypeScript, or documentation)
class UserRepository {
  async findById(id) { throw new Error('Not implemented'); }
  async findByEmail(email) { throw new Error('Not implemented'); }
  async create(userData) { throw new Error('Not implemented'); }
  async update(id, userData) { throw new Error('Not implemented'); }
  async delete(id) { throw new Error('Not implemented'); }
}

// Concrete implementation with Prisma
class PrismaUserRepository extends UserRepository {
  constructor(prisma) {
    super();
    this.prisma = prisma;
  }

  async findById(id) {
    return this.prisma.user.findUnique({ where: { id } });
  }

  async findByEmail(email) {
    return this.prisma.user.findUnique({ where: { email } });
  }

  async create(userData) {
    return this.prisma.user.create({ data: userData });
  }

  async update(id, userData) {
    return this.prisma.user.update({
      where: { id },
      data: userData,
    });
  }

  async delete(id) {
    return this.prisma.user.delete({ where: { id } });
  }
}

// Implementation for testing
class InMemoryUserRepository extends UserRepository {
  constructor() {
    super();
    this.users = new Map();
    this.idCounter = 1;
  }

  async findById(id) {
    return this.users.get(id) || null;
  }

  async create(userData) {
    const user = { id: this.idCounter++, ...userData };
    this.users.set(user.id, user);
    return user;
  }
  // ... other methods
}

// Service using the repository (dependency injection)
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async getUser(id) {
    const user = await this.userRepository.findById(id);
    if (!user) throw new Error('User not found');
    return user;
  }
}

Ten wzorzec pozwala na zmianę implementacji warstwy trwałości bez modyfikacji logiki biznesowej.

Pytanie 12: Jak zaimplementować system kolejek zadań?

Kolejki pozwalają na odraczanie ciężkich zadań i zapewniają ich niezawodne wykonanie.

job-queue.jsjavascript
const Queue = require('bull');

// Create queue with Redis as backend
const emailQueue = new Queue('email', {
  redis: {
    host: 'localhost',
    port: 6379,
  },
  defaultJobOptions: {
    attempts: 3,          // Number of attempts
    backoff: {
      type: 'exponential',
      delay: 2000,        // Initial delay between attempts
    },
    removeOnComplete: 100, // Keep last 100 completed jobs
  },
});

// Producer: add jobs to the queue
async function sendWelcomeEmail(userId, email) {
  await emailQueue.add('welcome', {
    userId,
    email,
    template: 'welcome',
  }, {
    priority: 1,          // High priority
    delay: 5000,          // 5-second delay
  });
}

// Consumer: process jobs
emailQueue.process('welcome', async (job) => {
  const { userId, email, template } = job.data;

  // Update progress
  job.progress(10);

  const html = await renderTemplate(template, { userId });
  job.progress(50);

  await sendEmail(email, 'Welcome!', html);
  job.progress(100);

  return { sent: true, email };
});

// Event handling
emailQueue.on('completed', (job, result) => {
  console.log(`Job ${job.id} completed:`, result);
});

emailQueue.on('failed', (job, err) => {
  console.error(`Job ${job.id} failed:`, err.message);
});

// Recurring jobs (cron)
emailQueue.add('newsletter', { type: 'weekly' }, {
  repeat: {
    cron: '0 9 * * MON', // Every Monday at 9am
  },
});

Bull z Redis to najpopularniejsze rozwiązanie. Dla prostszych potrzeb agenda lub bee-queue stanowią lekkie alternatywy.

Pytania zaawansowane

Pytanie 13: Jak działa natywny moduł N-API?

N-API pozwala tworzyć natywne moduły w C/C++ ze stabilnym API niezależnym od wersji Node.js.

native-module.cppcpp
// Native module for CPU-intensive calculations

#include <napi.h>

// Synchronous function exposed to JavaScript
Napi::Number Fibonacci(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  // Argument validation
  if (info.Length() < 1 || !info[0].IsNumber()) {
    Napi::TypeError::New(env, "Number expected")
      .ThrowAsJavaScriptException();
    return Napi::Number::New(env, 0);
  }

  int n = info[0].As<Napi::Number>().Int32Value();

  // Iterative Fibonacci calculation
  long long a = 0, b = 1;
  for (int i = 0; i < n; i++) {
    long long temp = a + b;
    a = b;
    b = temp;
  }

  return Napi::Number::New(env, static_cast<double>(a));
}

// Module initialization
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(
    Napi::String::New(env, "fibonacci"),
    Napi::Function::New(env, Fibonacci)
  );
  return exports;
}

NODE_API_MODULE(native_module, Init)
javascript
// Usage from JavaScript
const native = require('./build/Release/native_module');

// 10x faster than JavaScript equivalent
const result = native.fibonacci(50);

Moduły natywne przydają się przy intensywnych obliczeniach, integracji istniejących bibliotek C/C++ lub dostępie do API systemowych.

Pytanie 14: Wyjaśnij działanie Garbage Collectora V8

Zrozumienie GC pomaga pisać kod minimalizujący pauzy i zużycie pamięci.

gc-optimization.jsjavascript
// V8 GC uses two spaces: Young and Old Generation

// 1. Young Generation: short-lived objects
function shortLivedObjects() {
  for (let i = 0; i < 1000; i++) {
    const temp = { data: i }; // Allocated then collected quickly
  }
  // Minor GC (Scavenge) very fast
}

// 2. Old Generation: objects that survive multiple GCs
const cache = new Map(); // Survives, promoted to Old Generation

// ❌ Problematic pattern: many promoted objects
function createManyLongLived() {
  const objects = [];
  for (let i = 0; i < 100000; i++) {
    objects.push({ id: i, data: new Array(100).fill(0) });
  }
  return objects; // All promoted to Old Gen = slow major GC
}

// ✅ Optimized pattern: object reuse
class ObjectPool {
  constructor(factory, size = 100) {
    this.pool = Array.from({ length: size }, factory);
    this.available = [...this.pool];
  }

  acquire() {
    return this.available.pop() || this.pool[0];
  }

  release(obj) {
    // Reset and return to pool
    Object.keys(obj).forEach(k => obj[k] = null);
    this.available.push(obj);
  }
}

// GC monitoring
const v8 = require('v8');

function getHeapStats() {
  const stats = v8.getHeapStatistics();
  return {
    totalHeap: `${Math.round(stats.total_heap_size / 1024 / 1024)}MB`,
    usedHeap: `${Math.round(stats.used_heap_size / 1024 / 1024)}MB`,
    heapLimit: `${Math.round(stats.heap_size_limit / 1024 / 1024)}MB`,
  };
}

Flaga --max-old-space-size pozwala zwiększyć limit Old Generation dla aplikacji wymagających dużej ilości pamięci.

Pytanie 15: Jak zaimplementować graceful shutdown?

Graceful shutdown pozwala na zakończenie przetwarzania bieżących żądań i prawidłowe zamknięcie połączeń przed zatrzymaniem serwera.

graceful-shutdown.jsjavascript
const http = require('http');

const server = http.createServer((req, res) => {
  // Simulate a long request
  setTimeout(() => {
    res.writeHead(200);
    res.end('Done');
  }, 2000);
});

// Tracking active connections
let connections = new Set();

server.on('connection', (conn) => {
  connections.add(conn);
  conn.on('close', () => connections.delete(conn));
});

// Graceful shutdown function
async function shutdown(signal) {
  console.log(`${signal} received, starting graceful shutdown...`);

  // 1. Stop accepting new connections
  server.close(() => {
    console.log('HTTP server closed');
  });

  // 2. Close idle connections
  for (const conn of connections) {
    conn.end();
  }

  // 3. Close DB connections, queues, etc.
  await Promise.all([
    database.disconnect(),
    redisClient.quit(),
    messageQueue.close(),
  ]);

  // 4. Safety timeout
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 30000);

  console.log('Graceful shutdown completed');
  process.exit(0);
}

// Listen for termination signals
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

// Start server
server.listen(3000, () => {
  console.log('Server running on port 3000');
});

W produkcji z kontenerami (Docker, Kubernetes) graceful shutdown ma kluczowe znaczenie dla wdrożeń bez przestojów.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Pytania behawioralne

Pytanie 16: Opisz problem wydajnościowy, który udało się rozwiązać

To pytanie weryfikuje praktyczne doświadczenie. Odpowiedź powinna być ustrukturyzowana zgodnie z formatem STAR (Sytuacja, Zadanie, Działanie, Rezultat).

Przykład ustrukturyzowanej odpowiedzi:

text
Situation: A reporting API was timing out on requests
            exceeding 100,000 records.

Task:       Reduce response time from 45s to under 5s.

Action:
1. Profiling with clinic.js → identified JSON serialization as bottleneck
2. Implemented streaming with Transform streams
3. Database-side pagination
4. Added Redis caching for frequent queries

Result:     Response time reduced to 2s, memory usage decreased by 10x.

Pytanie 17: Jak zarządzać zależnościami i ich aktualizacjami?

package.json - Versioning best practicesjavascript
{
  "dependencies": {
    // ✅ Exact versions for production
    "express": "4.18.2",

    // ✅ Caret for compatible minor updates
    "lodash": "^4.17.21",

    // ❌ Avoid latest or *
    // "some-lib": "*"
  },
  "devDependencies": {
    // Quality tools
    "npm-check-updates": "^16.0.0"
  },
  "scripts": {
    // Vulnerability check
    "audit": "npm audit --audit-level=moderate",

    // Interactive update
    "update:check": "ncu",
    "update:apply": "ncu -u && npm install"
  },
  "engines": {
    // Specify required Node.js version
    "node": ">=20.0.0"
  }
}

Warto wspomnieć o stosowaniu package-lock.json, automatyzacji za pomocą Dependabot lub Renovate oraz o testach regresyjnych przed każdą dużą aktualizacją.

Podsumowanie

Rozmowy kwalifikacyjne z Node.js backend weryfikują zarówno teoretyczne zrozumienie wewnętrznych mechanizmów, jak i zdolność do rozwiązywania praktycznych problemów produkcyjnych. Opanowanie Event Loop, wzorców asynchronicznych i technik optymalizacji stanowi fundament oczekiwany od kandydatów na stanowiska seniorskich programistów backend.

Lista kontrolna przygotowania

  • ✅ Zrozumienie działania Event Loop i jego faz
  • ✅ Opanowanie różnic między callbackami, Promise i async/await
  • ✅ Znajomość wzorców obsługi błędów asynchronicznych
  • ✅ Wiedza, kiedy stosować strumienie zamiast klasycznych metod
  • ✅ Identyfikacja i naprawa wycieków pamięci
  • ✅ Stosowanie najlepszych praktyk bezpieczeństwa OWASP
  • ✅ Implementacja klasteryzacji i graceful shutdown
  • ✅ Korzystanie z narzędzi profilujących (clinic.js, Chrome DevTools)

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Przygotowanie techniczne warto uzupełnić projektami praktycznymi. Budowanie produkcyjnego API, udział w projektach open source związanych z Node.js lub rozwiązywanie zadań na platformach takich jak LeetCode pomaga ugruntować tę wiedzę.

Tagi

#nodejs
#interview
#backend
#javascript
#technical interview

Udostępnij

Powiązane artykuły