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 dell'event loop di Node.js?

L'event loop di libuv include fasi come `timers`, `pending callbacks`, `idle/prepare`, `poll`, `check` e `close callbacks`. La fase `timers` esegue le funzioni di callback di `setTimeout` e `setInterval`; la fase `poll` gestisce numerosi eventi di I/O (Input/Output); la fase `check` esegue le callback di `setImmediate`; la fase `close` gestisce gli eventi di chiusura come la disconnessione dei socket. Node.js dispone anche di code di microtask, che includono `process.nextTick` e i microtask delle `Promise`, le quali vengono eseguite tra una callback e l'altra. Un numero eccessivo di microtask può impedire all'I/O di avanzare e far sembrare il servizio bloccato.

Prova a rispondere a questa domanda con un coach IA

7In cosa differiscono le operazioni sincrone, asincrone e non bloccanti?

Un'operazione sincrona restituisce il risultato prima dell'esecuzione della riga successiva e blocca il thread principale, come `fs.readFileSync` in un gestore della richiesta. Un'operazione asincrona restituisce il suo risultato in un secondo momento tramite una funzione di callback o una `Promise`. Non bloccante significa che il thread può continuare a svolgere altro lavoro mentre l'operazione è in attesa. I termini sono correlati ma non identici: asincrono descrive come viene consegnato il risultato, mentre non bloccante descrive se il thread attende. I gestori delle richieste di backend dovrebbero generalmente preferire API asincrone e non bloccanti.

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

8Cos'è l'event loop starvation?

L'event loop starvation si verifica quando le operazioni JavaScript o le microtask tengono occupato l'event loop, impedendogli di elaborare tempestivamente l'I/O, i timer o le nuove richieste. Le cause comuni includono pesanti calcoli sincroni, elaborazione di JSON enormi, catene di Promise illimitate e l'uso eccessivo di process.nextTick. Le soluzioni includono la suddivisione del lavoro in blocchi, la limitazione delle dimensioni di input, la profilazione della CPU, lo spostamento del lavoro vincolato alla CPU (CPU-bound) su worker_threads o su un altro servizio, e l'evitare API sincrone nei percorsi di richiesta critici per le prestazioni.

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

9Cos'è una callback e cos'è il callback hell?

Una funzione di callback (o semplicemente callback) è una funzione passata per essere eseguita in un secondo momento, spesso al completamento di un'operazione asincrona. Le classiche callback di Node.js seguono generalmente lo stile 'error-first': callback(error, result). Il callback hell si verifica quando le operazioni dipendenti sono profondamente annidate, rendendo difficile la lettura del flusso di controllo e la gestione degli errori. La soluzione consiste nel scomporre il codice in funzioni con nome, utilizzare le Promise o async/await, ed eseguire le operazioni indipendenti tramite Promise.all invece di annidarle.

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

11In cosa differiscono process.nextTick, queueMicrotask, setImmediate e setTimeout?

`process.nextTick` viene eseguito dopo l'operazione corrente e prima che l'event loop avanzi. `queueMicrotask` programma un microtask JavaScript standard, simile alle continuazioni delle `Promise`. `setImmediate` viene eseguito nella fase check. `setTimeout(fn, 0)` viene eseguito nella fase timers dopo un ritardo minimo. Di solito `nextTick` viene eseguito prima dei microtask delle `Promise`, e l'ordine di `setTimeout(0)` e `setImmediate` dipende dal contesto; dopo i callback di I/O, `setImmediate` viene spesso eseguito per primo.

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
Prova a rispondere a questa domanda con un coach IA

12Come dovrebbero essere gestiti gli errori in Node.js?

Il codice sincrono utilizza try/catch. Il codice basato sulle Promise dovrebbe usare await all'interno di try/catch oppure concatenare un .catch. Le callback "error-first" dovrebbero verificare l'argomento dell'errore prima di utilizzare il risultato. Nelle applicazioni server, sono importanti un middleware centrale per gli errori, il logging nel contesto della richiesta, messaggi sicuri per il client e un arresto grazioso (graceful shutdown). uncaughtException non dovrebbe essere utilizzato per mantenere in esecuzione un processo corrotto; di solito è un segnale per registrare l'errore, eseguire la pulizia e riavviare.

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

14In che modo differiscono `require` di CommonJS e `import` di ES Modules?

CommonJS utilizza `require` e `module.exports`, mentre ES Modules utilizza `import` ed `export`. ES Modules supporta l'analisi statica e l'`await` a livello principale (top-level `await`), e viene selezionato tramite il campo `"type": "module"` nel `package.json` o utilizzando file `.mjs`. CommonJS viene spesso caricato in modo sincrono e può essere contrassegnato con `.cjs`. Nei progetti Node.js moderni esistono entrambi i sistemi, quindi una buona risposta evita semplificazioni eccessive e spiega che l'estensione del file, il `package.json` e il contesto del progetto determinano la modalità del modulo.

// CommonJS
const fs = require('node:fs');
module.exports = { calculate };

// ES Modules
import fs from 'node:fs';
export { calculate };
Prova a rispondere a questa domanda con un coach IA

15Cosa sono npm, package.json e package-lock.json?

npm è il gestore di pacchetti e la CLI (Command Line Interface). package.json descrive script, metadati, dependencies e devDependencies. package-lock.json registra l'esatto albero delle dipendenze in modo che le installazioni siano riproducibili. Nelle pipeline di CI (Continuous Integration), di solito si preferisce usare npm ci perché installa rigorosamente dal file di lock e fallisce se package.json e il file di lock non corrispondono. Questo protegge le build da variazioni accidentali delle dipendenze.

{
  "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 dovrebbero essere gestite la configurazione e le variabili d'ambiente?

Node.js legge le variabili d'ambiente tramite process.env, ma i valori sono stringhe e dovrebbero essere convalidati e convertiti all'avvio. I segreti non dovrebbero essere memorizzati nel repository, il file .env dovrebbe essere ignorato e i segreti in produzione dovrebbero provenire dalla piattaforma o da un gestore di segreti (secret manager). I servizi ben progettati si interrompono rapidamente (fail-fast) se manca la configurazione richiesta. NODE_ENV è una convenzione, non un perimetro di sicurezza, quindi il codice non dovrebbe fare affidamento su di esso come meccanismo di protezione.

const port = Number(process.env.PORT ?? 3000);
const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
  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);
console.log(buffer.toString('utf8'));

const safe = Buffer.alloc(1024);
const fast = Buffer.allocUnsafe(1024);
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';

const input = fs.createReadStream('large.log');
const output = fs.createWriteStream('copy.log');

input.pipe(output);
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