Preparazione ai colloqui backend su Node.js

Domande e risposte per colloqui su Node.js 2026

Un set mirato di domande da colloquio su Node.js per sviluppatori backend. Le risposte sono strutturate per la pratica orale: spiega l'architettura, fornisci un esempio e menziona i compromessi in produzione.

Inizia la simulazione di colloquio su Node.jsNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Architettura ed Event Loop

1Cos'è Node.js e per cosa viene utilizzato?

Node.js è un ambiente di esecuzione JavaScript al di fuori del browser, basato su V8 ed esteso con API per reti, file, processi, stream e accesso al sistema operativo. Viene comunemente utilizzato per API REST, servizi GraphQL, server WebSocket in tempo reale, microservizi, strumenti CLI, proxy e API gateway. Il suo punto di forza principale è l'I/O non bloccante: un singolo processo può gestire in modo efficiente molte connessioni di rete. Il compromesso è che calcoli lunghi e onerosi per la CPU possono bloccare l'event loop, a meno che non vengano spostati su worker, processi o un altro servizio.

Prova a rispondere a questa domanda con un coach IA

2Come funziona internamente Node.js?

JavaScript viene eseguito in V8, mentre le API di Node.js delegano le operazioni asincrone al sistema operativo o al pool di thread di libuv. Il thread principale di JavaScript non attende il completamento di un'operazione di I/O; continua a eseguire altro codice. Quando l'operazione è completata, la sua funzione di callback o la continuazione della Promise diventa pronta per l'event loop. È così che Node.js ottiene un'elevata concorrenza, anche se il codice JavaScript dell'utente viene generalmente eseguito su un singolo thread principale.

Prova a rispondere a questa domanda con un coach IA

3Cosa sono V8, libuv e l'event loop?

V8 compila ed esegue il codice JavaScript e ne gestisce gli oggetti e la memoria. libuv fornisce l'event loop multipiattaforma, l'integrazione dell'I/O asincrono e un pool di thread worker per operazioni come il lavoro sul filesystem, la risoluzione DNS e la crittografia. L'event loop decide quando devono essere eseguite le funzioni di callback delle operazioni completate. Node.js stesso collega le API JavaScript alle funzionalità native; da solo, V8 non fornisce un server HTTP, l'accesso al filesystem o le API di processo.

Prova a rispondere a questa domanda con un coach IA

4Perché Node.js è spesso definito single-thread?

Un singolo processo Node.js esegue solitamente il JavaScript dell'utente su un unico thread principale, quindi due normali gestori della richiesta non eseguono JavaScript esattamente nello stesso momento in quel thread. Tuttavia, la piattaforma non è letteralmente composta da un solo thread: libuv ha un pool di thread, il sistema operativo esegue l'I/O asincrono, V8 ha attività di garbage collection e Node.js supporta anche `worker_threads` e processi figlio. La definizione descrive principalmente il modello di esecuzione predefinito di JavaScript.

Prova a rispondere a questa domanda con un coach IA

5Come fa Node.js a gestire molte richieste in modo concorrente?

Node.js avvia le operazioni di I/O (Input/Output) e poi fa ritorno all'event loop invece di bloccare un thread per ogni richiesta. Ad esempio, 1.000 chiamate al database possono essere in corso mentre il thread principale continua ad accettare connessioni ed elaborare le funzioni di callback pronte. Questo approccio funziona bene per i carichi di lavoro legati all'I/O (I/O-bound) come chiamate HTTP, query al database, file e socket. Funziona in modo inefficiente quando i gestori delle richieste eseguono lavoro sincrono pesante per la CPU, poiché questo blocca l'event loop e ritarda ogni altra richiesta.

Prova a rispondere a questa domanda con un coach IA

6Quali sono le fasi principali del ciclo degli eventi di Node.js?

Node.js usa le fasi di pianificazione di libuv, tra cui pending callbacks, idle/prepare, poll, check e close callbacks, insieme all'elaborazione dei timer. poll gestisce molte callback di I/O (ingresso/uscita), check esegue le callback setImmediate e i timer eseguono le callback setTimeout/setInterval quando il ritardo minimo e la pianificazione lo consentono. Con libuv 1.45 e Node.js 20 o successivi, i timer vengono elaborati dopo poll a ogni iterazione, con un passaggio iniziale di compatibilità prima dell'ingresso nel ciclo. process.nextTick usa una coda separata di Node, mentre le reazioni alle Promise e queueMicrotask usano la coda delle microtask di V8; nessuna delle due è una fase di libuv. Queste code vengono elaborate nei punti di controllo tra le callback. Alimentarle ricorsivamente può impedire l'esecuzione di altro lavoro.

Prova a rispondere a questa domanda con un coach IA

7Come si distinguono le operazioni sincrone, asincrone e non bloccanti?

Sincrono descrive il flusso di controllo: una chiamata produce il risultato o genera un'eccezione prima che il chiamante prosegua. Asincrono descrive un risultato fornito successivamente tramite callback, Promise o un'altra notifica. Bloccante significa attendere un lavoro o una risorsa mentre il thread corrente non può svolgere altro lavoro; le interfacce non bloccanti evitano tale attesa, per esempio restituendo un risultato «bloccherebbe» oppure predisponendo una notifica. Sono dimensioni diverse: sincrono non significa da solo che un'interfaccia attenda la disponibilità dell'I/O (ingresso/uscita), e restituire una Promise non rende non bloccante un calcolo sincrono pesante. In Node.js fs.readFileSync è un'interfaccia sincrona bloccante; fs.promises.readFile fornisce il risultato in modo asincrono mentre il thread principale può elaborare altro lavoro. Nei percorsi di gestione delle richieste preferisci interfacce asincrone di I/O adatte.

import fs from 'node:fs';

const syncData = fs.readFileSync('file.txt', 'utf8');

const asyncData = await fs.promises.readFile(
  'file.txt',
  'utf8'
);
Prova a rispondere a questa domanda con un coach IA

8Che cos'è la starvation del ciclo degli eventi?

La starvation, cioè la mancata disponibilità di tempo di esecuzione per altre attività, si verifica quando JavaScript sincrono o code dei punti di controllo continuamente alimentate impediscono di elaborare tempestivamente callback di I/O (ingresso/uscita), timer e richieste. Le cause comprendono calcoli pesanti, grandi operazioni JSON, microtask pianificate ricorsivamente e uso eccessivo di process.nextTick. Limita le dimensioni degli input, analizza con un profiler il lavoro costoso e sposta i compiti intensivi di calcolo su worker_threads o un altro servizio. Se dividi il lavoro in blocchi, cedi il controllo a un successivo giro del ciclo, per esempio con setImmediate; attendere ripetutamente Promise già risolte o pianificare queueMicrotask può comunque affamare l'I/O. Evita interfacce sincrone bloccanti nei percorsi delle richieste eseguiti più spesso.

app.get('/report', (req, res) => {
  const result = performVeryHeavyCalculation();
  res.json(result);
});
Prova a rispondere a questa domanda con un coach IA

Programmazione asincrona ed errori

9Che cos'è una callback e che cosa si intende per callback hell?

Una callback è una funzione passata a un'altra operazione affinché quest'ultima la invochi. Può essere eseguita in modo sincrono, come con Array.map, o asincrono, come con fs.readFile. Le callback asincrone classiche di Node.js usano comunemente la convenzione con l'errore come primo argomento: callback(error, result). La callback hell, o «inferno delle callback», consiste in callback dipendenti profondamente annidate che rendono poco chiari il flusso di controllo e la gestione degli errori. Funzioni con un nome, operazioni più piccole, Promise e async/await possono migliorare la leggibilità. Avvia insieme le operazioni indipendenti quando opportuno, per esempio con Promise.all, rispettando i limiti di concorrenza e gestendo gli errori.

import fs from 'node:fs';

fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(data);
});
Prova a rispondere a questa domanda con un coach IA

10Cos'è una Promise e come funziona `async/await`?

Una `Promise` rappresenta il risultato futuro di un'operazione asincrona e può essere in stato pending (in sospeso), fulfilled (soddisfatta) o rejected (rifiutata). Una funzione `async` restituisce sempre una `Promise`. `await` mette in pausa solo la funzione `async` corrente fino a quando la `Promise` non viene risolta (settled); non blocca l'intero processo Node.js. Una buona risposta dovrebbe menzionare la gestione degli errori con `try/catch`, l'importanza di evitare Promise ignorate (floating Promise) e l'utilizzo di `Promise.all` per lavori indipendenti, in modo che le operazioni non vengano eseguite inutilmente in sequenza.

async function loadUser(id) {
  try {
    const user = await repository.findById(id);
    return user;
  } catch (error) {
    console.error(error);
    throw error;
  }
}

const [user, settings] = await Promise.all([
  getUser(),
  getSettings()
]);
Prova a rispondere a questa domanda con un coach IA

11Qual è la differenza tra process.nextTick, queueMicrotask, setImmediate e setTimeout?

process.nextTick pianifica lavoro nella coda next-tick di Node; queueMicrotask usa la coda delle microtask di V8, usata anche dalle reazioni alle Promise. In un normale punto di controllo al livello principale di CommonJS, le callback nextTick precedono le callback Promise/queueMicrotask in coda. Al livello principale di un modulo ES, la valutazione avviene già in un contesto di microtask, quindi le microtask in coda possono precedere nextTick. È un comportamento dipendente dal contesto, non una regola universale di priorità che interrompe JavaScript in esecuzione. setImmediate viene eseguito in check. setTimeout(fn, 0) attende una soglia temporale minima e non è immediato. Il loro ordine relativo dipende da dove vengono pianificati; quando entrambi sono pianificati all'interno di una callback di I/O (ingresso/uscita), setImmediate viene eseguito per primo. Preferisci queueMicrotask per rimandare un'operazione a una microtask in modo portabile, salvo quando serve il comportamento specifico di nextTick.

// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
Prova a rispondere a questa domanda con un coach IA

12Come si dovrebbero gestire gli errori in Node.js?

Usa try/catch per le eccezioni sincrone. Per operazioni basate su Promise, usa await dentro try/catch o aggiungi un gestore del rifiuto; un try/catch sincrono esterno non intercetta un'eccezione lanciata da una callback successiva. Le callback con l'errore come primo argomento devono controllarlo e gli eventi error di EventEmitter richiedono gestori appropriati. I server dovrebbero centralizzare la gestione degli errori delle richieste, registrare informazioni di contesto utili ed evitare di esporre dettagli interni ai client. Dopo una uncaughtException, non presumere che il processo possa continuare in sicurezza: esegui solo la pulizia di emergenza sincrona necessaria, termina il processo e lascia che un supervisore lo riavvii. Un arresto ordinato pianificato è utile per la terminazione normale, ma un recupero asincrono arbitrario dopo un'eccezione non gestita non è una strategia affidabile.

try {
  JSON.parse(input);
} catch (error) {
  handleError(error);
}

try {
  await operation();
} catch (error) {
  handleError(error);
}

operation((error, result) => {
  if (error) return handleError(error);
  return result;
});
Prova a rispondere a questa domanda con un coach IA

Moduli e gestione del progetto

13Cosa sono i moduli in Node.js?

Un modulo è un'unità isolata di codice con un'interfaccia esportata esplicita. Node.js supporta moduli integrati come `node:fs` e `node:http`, moduli locali del progetto, pacchetti npm, moduli CommonJS e moduli ECMAScript. I moduli rendono più chiari i confini di responsabilità e consentono il riutilizzo. Il compromesso in produzione è l'accoppiamento: se i moduli si importano a vicenda in modo troppo profondo o nascondono effetti collaterali al momento dell'importazione, il testing e il comportamento all'avvio diventano più difficili da analizzare.

// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
Prova a rispondere a questa domanda con un coach IA

14Qual è la differenza tra require di CommonJS e import dei moduli ES?

CommonJS usa require e module.exports; i moduli ES usano import ed export. require carica normalmente in modo sincrono e può essere chiamato condizionalmente. Le dichiarazioni import statiche rendono il grafo delle dipendenze disponibile all'analisi; import() dinamico restituisce una Promise. Le versioni moderne di Node.js possono anche caricare con require un grafo di moduli ES privo di await al livello principale, nel rispetto delle regole di interoperabilità supportate. I moduli ES non implicano quindi una valutazione sempre asincrona. Le estensioni .cjs/.mjs, il campo type di package.json e le regole applicabili di riconoscimento della sintassi determinano la modalità del modulo. I moduli ES supportano await al livello principale. CommonJS fornisce __dirname/__filename; le versioni attuali di Node.js forniscono import.meta.dirname/filename per i moduli ES basati su file, mentre import.meta.url è utile per la risoluzione tramite URL e per approcci compatibili con versioni precedenti.

// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };

// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
Prova a rispondere a questa domanda con un coach IA

15Che cosa sono npm, package.json e package-lock.json?

npm è l'interfaccia a riga di comando per la gestione dei pacchetti. package.json descrive metadati, script e requisiti delle dipendenze dirette, spesso tramite intervalli di versioni. package-lock.json registra un albero risolto delle dipendenze con versioni dei pacchetti e informazioni di integrità. Includi il file di lock di un'applicazione nel controllo di versione. npm ci è utile nell'integrazione continua perché installa dal file di lock, rimuove una directory node_modules esistente e fallisce se package.json e il lock non concordano. Il file di lock riduce le variazioni involontarie delle dipendenze; non rende identici sistemi operativi, compilazioni native, script del ciclo di vita, versioni di Node od opzioni di installazione. Per compilazioni riproducibili servono anche una catena di strumenti e un ambiente controllati.

{
  "name": "api",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "test": "node --test"
  },
  "dependencies": {},
  "devDependencies": {}
}
Prova a rispondere a questa domanda con un coach IA

16Come vanno gestite la configurazione e le variabili d'ambiente?

Leggi le variabili d'ambiente tramite process.env: i valori presenti sono stringhe, mentre una proprietà assente vale undefined. Valida i valori richiesti e convertili intenzionalmente all'avvio; convertire PORT soltanto con Number non rifiuta una stringa vuota, NaN o una porta fuori dall'intervallo ammesso. Interrompi l'avvio con un errore chiaro quando la configurazione richiesta non è valida. Tieni i segreti reali fuori dal controllo di versione e fornisci quelli di produzione tramite la piattaforma di distribuzione o un gestore di segreti; una configurazione di esempio senza segreti può essere versionata. NODE_ENV è una convenzione applicativa, non un confine di sicurezza attendibile.

const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
  throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
Prova a rispondere a questa domanda con un coach IA

Eventi, buffer e stream

17Cos'è `EventEmitter`?

`EventEmitter` implementa un pattern publish-subscribe all'interno di un processo Node.js. `on` registra un ascoltatore (listener), `once` registra un listener per un singolo evento, `off` rimuove un listener ed `emit` chiama in modo sincrono i listener nell'ordine di registrazione. La natura sincrona è importante: un listener lento blocca l'emettitore. L'evento `error` è speciale perché un evento `error` non gestito può causare il crash del processo. I sistemi a lunga esecuzione (long-lived) dovrebbero anche rimuovere i listener inutilizzati per evitare perdite di memoria (memory leak).

import { EventEmitter } from 'node:events';

const emitter = new EventEmitter();

emitter.on('orderCreated', order => {
  console.log(`Order ${order.id} created`);
});

emitter.emit('orderCreated', { id: 42 });
Prova a rispondere a questa domanda con un coach IA

18A cosa serve Buffer?

Buffer rappresenta una sequenza di byte a dimensione fissa. Viene utilizzato per file, socket TCP (Transmission Control Protocol), corpi HTTP (Hypertext Transfer Protocol), immagini, crittografia e protocolli binari. Buffer.from crea un buffer da dati esistenti, Buffer.alloc crea memoria riempita con zeri, e Buffer.allocUnsafe è più veloce ma può contenere vecchia memoria finché non viene sovrascritta. Questo rende allocUnsafe appropriato solo quando il codice riempie immediatamente il buffer e non espone mai dati residui.

const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
Prova a rispondere a questa domanda con un coach IA

19Cosa sono gli stream e quali tipi esistono?

Gli stream elaborano i dati a blocchi (chunk) invece di caricare tutto in memoria. I tipi principali sono Readable, Writable, Duplex e Transform. I file, gli oggetti di richiesta e risposta HTTP (Hypertext Transfer Protocol), i socket TCP (Transmission Control Protocol) e gli stream gzip ne sono esempi comuni. Gli stream sono preziosi per i file di grandi dimensioni e per il traffico di rete poiché riducono l'uso della memoria e la latenza. Una risposta per ambienti di produzione dovrebbe menzionare che gli errori degli stream devono essere gestiti correttamente lungo l'intera catena.

import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
try {
  await pipeline(
    fs.createReadStream('large.log'),
    fs.createWriteStream('copy.log')
  );
} catch (error) {
  console.error('Copy failed:', error);
  process.exitCode = 1;
}
Prova a rispondere a questa domanda con un coach IA

20Cos'è la backpressure e perché `pipeline` è utile?

La backpressure (contropressione) si verifica quando una sorgente di dati produce blocchi (chunk) più velocemente di quanto la destinazione riesca a consumarli. Senza backpressure, la memoria può crescere fino a rallentare o far arrestare in modo anomalo il processo. Gli stream e `pipe` possono coordinare il flusso, ma `pipeline` di `node:stream/promises` è più sicuro per catene a più passaggi perché propaga gli errori, chiude gli stream correlati e restituisce una `Promise`. È un'ottima scelta predefinita per la compressione di file, gli upload, i download e le trasformazioni di stream.

import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';

await pipeline(
  fs.createReadStream('input.txt'),
  zlib.createGzip(),
  fs.createWriteStream('input.txt.gz')
);
Prova a rispondere a questa domanda con un coach IA