Node.js-työhaastattelukysymykset ja vastaukset 2026
Kohdennettu Node.js-haastattelukysymysten kokoelma backend-kehittäjille. Vastaukset on jäsennelty suullista harjoittelua varten: selitä arkkitehtuuri, anna esimerkki ja mainitse tuotantoympäristön kompromissit.
Node.js on selaimen ulkopuolinen JavaScript-ajoympäristö, joka perustuu V8-moottoriin ja jota on laajennettu API-rajapinnoilla verkko-operaatioiden, tiedostojen, prosessien, tietovirtojen ja käyttöjärjestelmäresurssien käsittelyyn. Sitä käytetään yleisesti REST API -rajapintojen, GraphQL-palveluiden, reaaliaikaisten WebSocket-palvelimien, mikropalveluiden, CLI (Command Line Interface) -työkalujen, proxy-palvelimien ja API-yhdyskäytävien toteuttamiseen. Sen suurin vahvuus on ei-estävät I/O-operaatiot (Input/Output): yksi prosessi pystyy käsittelemään tehokkaasti useita verkkoyhteyksiä. Kompromissina on se, että pitkät ja paljon suoritinaikaa vaativat laskutoimitukset voivat estää tapahtumasilmukan etenemisen, ellei niitä siirretä erillisille `worker`-säikeille, prosesseille tai toiseen palveluun.
JavaScript suoritetaan V8-moottorissa, kun taas Node.js:n API-rajapinnat (Application Programming Interface) siirtävät asynkronisen työn käyttöjärjestelmälle tai libuv:n säiepoolille. JavaScriptin pääsäie ei jää odottamaan I/O-operaation valmistumista, vaan se jatkaa muun koodin suorittamista. Kun operaatio valmistuu, sen takaisinkutsu tai `Promise`-jatkokutsu on valmiina tapahtumasilmukkaa varten. Näin Node.js saavuttaa korkean rinnakkaisuuden, vaikka käyttäjän JavaScript-koodi suoritetaan yleensä yhdessä pääsäikeessä.
V8 kääntää ja suorittaa JavaScriptiä sekä hallitsee JavaScript-olioita ja muistia. libuv tarjoaa monialustaisen tapahtumasilmukan, asynkronisen I/O-integraation sekä säiepoolin toiminnoille, kuten joillekin tiedostojärjestelmäoperaatioille, DNS-kyselyille ja kryptografialle. Tapahtumasilmukka päättää, milloin valmistuneiden toimintojen takaisinkutsut tulisi suorittaa. Itse Node.js yhdistää JavaScriptin API-rajapinnat natiiveihin kyvykkyyksiin; V8 yksinään ei tarjoa HTTP-palvelinta, pääsyä tiedostojärjestelmään tai prosessin rajapintoja.
4Miksi Node.js:ää kutsutaan usein yksisäikeiseksi?
Yksi Node.js-prosessi suorittaa käyttäjän JavaScript-koodia yleensä yhdellä pääsäikeellä, joten kaksi tavallista pyynnön käsittelijää eivät suorita JavaScriptiä täsmälleen samaan aikaan kyseisessä säikeessä. Alusta ei kuitenkaan ole kirjaimellisesti yksisäikeinen: libuv-kirjastossa on säiepooli, käyttöjärjestelmä hoitaa asynkronista I/O:ta, V8-moottorilla on omat roskienkeruutoimenpiteensä ja Node.js tukee myös `worker_threads`-moduulia sekä lapsiprosesseja. Ilmaisu kuvaa pääasiassa JavaScriptin oletusarvoista suoritusmallia.
5Miten Node.js pystyy käsittelemään monia pyyntöjä samanaikaisesti?
Node.js käynnistää I/O-operaatiot ja palaa sen jälkeen tapahtumasilmukkaan sen sijaan, että se varaisi yhden säikeen jokaista pyyntöä kohden ja jäisi odottamaan. Esimerkiksi 1 000 tietokantakutsua voi olla käynnissä samanaikaisesti, kun pääsäie jatkaa uusien yhteyksien vastaanottamista ja valmiiden takaisinkutsujen käsittelyä. Tämä toimii hyvin I/O-intensiivisissä työkuormissa, kuten HTTP-kutsuissa, tietokantakyselyissä, tiedostojen käsittelyssä ja verkkosocketeissa. Se toimii huonosti silloin, jos pyyntöjen käsittelijät tekevät raskasta synkronista CPU-työtä, koska se tukkii tapahtumasilmukan ja viivästyttää kaikkia muita pyyntöjä.
6Mitkä ovat Node.js:n tapahtumasilmukan päävaiheet?
Node.js käyttää libuvin vaiheita, kuten pending callbacks, idle/prepare, poll, check ja close callbacks, sekä ajastinkäsittelyä. poll käsittelee monia I/O-takaisinkutsuja (syöttö ja tulostus), check suorittaa setImmediate-kutsut ja ajastimet setTimeout/setInterval-kutsut vähimmäisviiveen ja ajoituksen salliessa. libuv 1.45:ssä ja Node.js 20+:ssa ajastimet käsitellään joka kierroksella poll-vaiheen jälkeen, ja ennen silmukkaa tehdään yhteensopivuuden vuoksi ensimmäinen ajastinkierros. process.nextTick käyttää erillistä Noden jonoa, Promise-reaktiot ja queueMicrotask V8:n mikrotehtäväjonoa; kumpikaan ei ole libuvin vaihe. Jonot suoritetaan takaisinkutsujen välisissä tarkistuspisteissä. Rekursiivinen täydentäminen voi estää muun työn etenemisen.
7Miten synkroniset, asynkroniset ja estämättömät operaatiot eroavat toisistaan?
Synkronisuus kuvaa ohjausvirtaa: kutsu tuottaa tuloksen tai heittää poikkeuksen ennen kutsujan jatkamista. Asynkronisuus kuvaa myöhemmin takaisinkutsulla, Promisella tai muulla ilmoituksella toimitettua tulosta. Estäminen tarkoittaa työn tai resurssin odottamista niin, ettei nykyinen säie voi tehdä muuta. Estämättömät rajapinnat välttävät odotuksen esimerkiksi palauttamalla ”operaatio estäisi” tai järjestämällä ilmoituksen. Nämä ovat eri ominaisuuksia: synkronisuus ei yksin tarkoita I/O-valmiuden (syötön ja tulostuksen) odottamista, eikä Promisen palauttaminen tee raskaasta synkronisesta laskennasta estämätöntä. fs.readFileSync on estävä synkroninen rajapinta; fs.promises.readFile toimittaa tuloksen asynkronisesti pääsäikeen voidessa tehdä muuta. Suosi pyyntöjen käsittelyssä sopivia asynkronisia I/O-rajapintoja.
Synkroninen JavaScript tai jatkuvasti täydentyvät tarkistuspisteiden jonot voivat estää I/O-takaisinkutsujen (syötön ja tulostuksen), ajastimien ja pyyntöjen oikea-aikaisen käsittelyn. Syitä ovat raskaat laskennat, suuret JSON-operaatiot, rekursiivisesti ajoitetut mikrotehtävät ja liiallinen process.nextTick. Rajoita syötteiden kokoa, profiloi kallis työ ja siirrä raskas laskenta worker_threads-säikeisiin tai toiseen palveluun. Jakaessasi työn osiin luovuta hallinta myöhemmälle tapahtumasilmukan kierrokselle, esimerkiksi setImmediatella; toistuva jo ratkaistujen Promise-olioiden odottaminen tai queueMicrotask voi edelleen nälkiinnyttää I/O:n. Vältä estäviä synkronisia rajapintoja usein suoritetuilla pyyntöpoluilla.
9Mikä on takaisinkutsu ja mitä takaisinkutsuhelvetti tarkoittaa?
Takaisinkutsu on toiselle operaatiolle annettu funktio, jota tämä operaatio kutsuu. Se voi suorittua synkronisesti kuten Array.mapissa tai asynkronisesti kuten fs.readFilessä. Perinteiset asynkroniset Node.js-takaisinkutsut käyttävät usein virhettä ensimmäisenä argumenttina: callback(error, result). Takaisinkutsuhelvetti tarkoittaa syvästi sisäkkäisiä riippuvaisia kutsuja, jotka hämärtävät ohjausvirtaa ja virheenkäsittelyä. Nimetyt funktiot, pienemmät operaatiot, Promise ja async/await parantavat luettavuutta. Käynnistä riippumattomat operaatiot tarvittaessa yhdessä esimerkiksi Promise.allilla, noudattaen samanaikaisuusrajoja ja käsitellen virheet.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
`Promise` edustaa asynkronisen toiminnon tulevaa tulosta, ja se voi olla odottava, toteutunut tai hylätty. `async`-funktio palauttaa aina `Promise`-olion. `await` keskeyttää vain nykyisen `async`-funktion suorituksen, kunnes `Promise` ratkeaa; se ei pysäytä koko Node.js-prosessia. Hyvässä vastauksessa tulisi mainita virheiden käsittely `try/catch`-rakenteella, kelluvien `Promise`-olioiden välttäminen ja `Promise.all`-metodin käyttö riippumattomissa toiminnoissa, jotta operaatioita ei suoriteta tarpeettomasti peräkkäin.
11Miten process.nextTick, queueMicrotask, setImmediate ja setTimeout eroavat toisistaan?
process.nextTick ajoittaa työn Noden next-tick-jonoon; queueMicrotask käyttää V8:n mikrotehtäväjonoa, jota myös Promise-reaktiot käyttävät. Tavallisessa CommonJS:n ylätason tarkistuspisteessä nextTick-kutsut suoritetaan ennen jonossa olevia Promise/queueMicrotask-kutsuja. ES-moduulin ylätason arviointi tapahtuu jo mikrotehtäväkontekstissa, joten jonon mikrotehtävät voivat edeltää nextTickiä. Tämä riippuu kontekstista eikä ole yleinen käynnissä olevan JavaScriptin keskeyttävä prioriteettisääntö. setImmediate suoritetaan check-vaiheessa. setTimeout(fn, 0) odottaa vähimmäisviivettä eikä ole välitön. Niiden keskinäinen järjestys riippuu ajoituspaikasta; jos molemmat ajoitetaan I/O-takaisinkutsussa (syöttö ja tulostus), setImmediate suoritetaan ensin. Suosi queueMicrotaskia siirrettävään mikrotehtävälykkäykseen, ellei nextTickin erityiskäyttäytymistä tarvita.
// 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.
Käytä try/catchia synkronisiin poikkeuksiin. Promise-operaatioissa käytä awaitia try/catchin sisällä tai lisää hylkäyskäsittelijä; ulompi synkroninen try/catch ei sieppaa myöhemmän takaisinkutsun poikkeusta. Virheen ensimmäisenä argumenttina saavat takaisinkutsut tarkistavat sen, ja EventEmitterin error-tapahtumat tarvitsevat asianmukaiset käsittelijät. Palvelinten tulee keskittää pyyntövirheiden käsittely, kirjata hyödyllinen konteksti ja välttää sisäisten tietojen paljastamista asiakkaille. uncaughtException-tapahtuman jälkeen prosessin turvallista jatkamista ei voi olettaa: tee vain välttämätön synkroninen hätäsiivous, lopeta ja anna valvontaprosessin käynnistää prosessi uudelleen. Hallittu alasajo sopii normaaliin lopettamiseen, mutta mielivaltainen asynkroninen toipuminen sieppaamattomasta poikkeuksesta ei ole luotettava strategia.
Moduuli on eristetty koodiyksikkö, jolla on eksplisiittisesti viety rajapinta. Node.js tukee sisäänrakennettuja moduuleja, kuten `node:fs` ja `node:http`, paikallisia projektimoduuleja, npm-paketteja sekä CommonJS- ja ECMAScript-moduuleja. Moduulit selkeyttävät vastuualueiden rajoja ja mahdollistavat uudelleenkäytettävyyden. Tuotantokäytön kompromissina on kytkös: jos moduulit tuovat toisiaan liian syvällä tasolla tai piilottavat sivuvaikutuksia tuontivaiheessa, testauksesta ja käynnistyskäyttäytymisestä tulee vaikeammin ennakoitavaa.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Miten CommonJS:n require ja ES-moduulien import eroavat?
CommonJS käyttää requirea ja module.exportsia; ES-moduulit importia ja exportia. require lataa yleensä synkronisesti ja sitä voi kutsua ehdollisesti. Staattiset import-määrittelyt paljastavat riippuvuusgraafin analyysille; dynaaminen import() palauttaa Promisen. Nykyinen Node.js voi myös ladata requirella ES-moduuligraafin, jossa ei ole ylätason awaitia, tuettujen yhteentoimivuussääntöjen mukaan. ES-moduulit eivät siis tarkoita aina asynkronista arviointia. .cjs/.mjs, package.jsonin type ja sovellettavat syntaksintunnistussäännöt määrittävät moduulitilan. ES-moduulit tukevat ylätason awaitia. CommonJS tarjoaa __dirname/__filename-arvot; nykyinen Node.js tarjoaa tiedostopohjaisille ES-moduuleille import.meta.dirname/filename-arvot. import.meta.url soveltuu URL-pohjaiseen ratkaisuun ja vanhempien versioiden kanssa yhteensopiviin menetelmiin.
// 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.
15Mitä ovat npm, package.json ja package-lock.json?
npm on paketinhallinnan komentorivikäyttöliittymä. package.json kuvaa metatiedot, skriptit ja suorien riippuvuuksien vaatimukset, usein versioalueina. package-lock.json tallentaa ratkaistun riippuvuuspuun pakettiversioineen ja eheystietoineen. Tallenna sovelluksen lukitustiedosto versionhallintaan. npm ci sopii jatkuvaan integraatioon: se asentaa lukitustiedostosta, poistaa olemassa olevan node_modules-hakemiston ja epäonnistuu, jos package.json ja lukitus eivät vastaa toisiaan. Lukitustiedosto vähentää riippuvuuksien ajautumista, mutta ei yhdenmukaista käyttöjärjestelmiä, natiivikäännöksiä, elinkaariskriptejä, Node-versioita tai asennusvalintoja. Toistettavat käännökset vaativat myös hallitun työkaluketjun ja ympäristön.
16Miten asetuksia ja ympäristömuuttujia tulisi hallita?
Lue ympäristömuuttujat process.envin kautta: olemassa olevat arvot ovat merkkijonoja ja puuttuva ominaisuus on undefined. Validoi pakolliset arvot ja muunna ne tarkoituksellisesti käynnistyksessä; pelkkä PORTin Number-muunnos ei hylkää tyhjää merkkijonoa, NaN-arvoa tai sallitun alueen ulkopuolista porttia. Keskeytä selkeällä virheellä, jos pakollinen asetus on virheellinen. Pidä oikeat salaisuudet poissa versionhallinnasta ja toimita tuotannon salaisuudet käyttöönottoalustasta tai salaisuuksien hallinnasta; salaisuuksia sisältämätön esimerkkikonfiguraatio voidaan versionhallita. NODE_ENV on sovelluskäytäntö, ei luotettava turvaraja.
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');
`EventEmitter` toteuttaa julkaisija-tilaaja -mallin Node.js-prosessin sisällä. `on` rekisteröi kuuntelijan, `once` rekisteröi kuuntelijan yhtä tapahtumaa varten, `off` poistaa kuuntelijan ja `emit` kutsuu kuuntelijoita synkronisesti niiden rekisteröintijärjestyksessä. Synkroninen luonne on tärkeä: hidas kuuntelija estää `EventEmitter`:n suorituksen. `error`-tapahtuma on poikkeuksellinen, sillä käsittelemätön virhetapahtuma voi kaataa koko prosessin. Pitkään ajettavissa järjestelmissä tulisi myös muistaa poistaa käyttämättömät kuuntelijat muistivuotojen välttämiseksi.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
`Buffer` edustaa kiinteän kokoista tavujonoa. Sitä käytetään tiedostojen, TCP-verkkosocketien (Transmission Control Protocol), HTTP-viestien (Hypertext Transfer Protocol) runkojen, kuvien, kryptografian ja binääriprotokollien käsittelyyn. `Buffer.from` luo puskurin olemassa olevasta datasta, `Buffer.alloc` varaa nollilla täytettyä muistia ja `Buffer.allocUnsafe` on nopeampi, mutta se saattaa sisältää vanhaa muistidataa, kunnes se ylikirjoitetaan. Tämän vuoksi `allocUnsafe` sopii käytettäväksi vain silloin, kun koodi täyttää puskurin välittömästi eikä koskaan paljasta vanhaa dataa.
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.
19Mitä ovat tietovirrat ja mitä tyyppejä on olemassa?
Tietovirrat käsittelevät dataa palasina sen sijaan, että koko datamäärä ladattaisiin kerralla muistiin. Päätyypit ovat `Readable`, `Writable`, `Duplex` ja `Transform`. Tiedostot, HTTP (Hypertext Transfer Protocol) -pyyntö- ja -vastausoliot, TCP (Transmission Control Protocol) -socketit ja gzip-tietovirrat ovat yleisiä esimerkkejä. Tietovirrat ovat erittäin hyödyllisiä suurten tiedostojen ja verkkoliikenteen käsittelyssä, koska ne vähentävät muistinkulutusta ja viivettä. Tuotantotason vastauksessa tulisi mainita, että tietovirtojen virheet on käsiteltävä oikein koko ketjun läpi.
20Mitä on vastapaine (backpressure) ja miksi `pipeline` on hyödyllinen?
Vastapaine syntyy, kun tietolähde tuottaa dataa nopeammin kuin kohde ehtii sitä käsitellä. Ilman vastapainetta muistin kulutus voi kasvaa, kunnes prosessi hidastuu tai kaatuu. Tietovirrat ja `pipe` voivat koordinoida tiedonkulkua, mutta `node:stream/promises`-moduulin `pipeline` on turvallisempi monivaiheisissa ketjuissa, koska se välittää virheet eteenpäin, sulkee ketjuun kuuluvat tietovirrat ja palauttaa `Promise`-olion. Se on hyvä oletusvalinta tiedostojen pakkaukseen, tiedostojen lähetykseen ja lataukseen sekä tietovirtojen muunnoksiin.
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')
);