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?
Libuv-kirjaston tapahtumasilmukka sisältää vaiheita kuten `timers`, `pending callbacks`, `idle/prepare`, `poll`, `check` ja `close callbacks`. `timers`-vaihe suorittaa `setTimeout`- ja `setInterval`-funktioiden takaisinkutsut; `poll`-vaihe käsittelee useita I/O-tapahtumia; `check`-vaihe suorittaa `setImmediate`-takaisinkutsut; `close`-vaihe käsittelee sulkemistapahtumia, kuten verkkosocketien sulkemisen. Node.js:ssä on myös mikrotehtäväjonoja (microtask queues), mukaan lukien `process.nextTick` ja `Promise`-mikrotehtävät, jotka suoritetaan takaisinkutsujen välissä. Liian monet mikrotehtävät voivat estää I/O:n etenemistä ja saada palvelun vaikuttamaan jumiutuneelta.
7Miten synkroniset, asynkroniset ja ei-estävät (non-blocking) toiminnot eroavat toisistaan?
Synkroninen toiminto palauttaa arvonsa ennen seuraavan rivin suorittamista ja pysäyttää pääsäikeen, mistä esimerkkinä on `fs.readFileSync` pyynnön käsittelijässä. Asynkroninen toiminto palauttaa tuloksensa myöhemmin takaisinkutsun tai `Promise`-olion kautta. Ei-estävä tarkoittaa, että säie voi jatkaa muiden töiden tekemistä toiminnon ollessa kesken. Termit liittyvät toisiinsa, mutta eivät ole identtisiä: asynkroninen kuvaa tuloksen toimitustapaa, kun taas ei-estävä kuvaa sitä, joutuuko säie odottamaan. Taustajärjestelmien pyyntöjen käsittelijöissä tulisi yleensä suosia asynkronisia ja ei-estäviä rajapintoja.
8Mitä tarkoittaa tapahtumasilmukan nälkiintyminen (event loop starvation)?
Tapahtumasilmukan nälkiintyminen tapahtuu, kun JavaScript-suoritus tai mikrotehtävät pitävät tapahtumasilmukan niin varattuna, ettei se ehdi käsitellä I/O-operaatioita, ajastimia tai uusia pyyntöjä viipymättä. Yleisiä syitä ovat raskaat synkroniset laskutoimitukset, valtavien JSON-rakenteiden käsittely, päättymättömät `Promise`-ketjut ja liiallinen `process.nextTick`-kutsujen käyttö. Ratkaisuja ovat työn pilkkominen osiin, syötteiden kokojen rajoittaminen, prosessorin käytön profilointi, prosessorisidonnaisen työn siirtäminen `worker_threads`-säikeisiin tai toiseen palveluun sekä synkronisten API-kutsujen välttäminen suorituskyvyn kannalta kriittisillä pyyntöreiteillä.
9Mikä on takaisinkutsu ja mitä tarkoitetaan callback hell -ilmiöllä?
Takaisinkutsu on funktio, joka välitetään suoritettavaksi myöhemmin, usein asynkronisen toiminnon valmistumisen jälkeen. Perinteiset Node.js-takaisinkutsut noudattavat yleensä error-first -tyyliä: `callback(error, result)`. Callback hell (takaisinkutsuhelvetti) syntyy, kun riippuvaisia toimintoja ketjutetaan syvälle toistensa sisään, mikä tekee virheiden käsittelystä ja suorituksen ohjauksesta vaikeasti luettavaa. Ratkaisuna on jakaa koodi nimettyihin funktioihin, käyttää Promiseja tai `async/await`-syntaksia ja suorittaa riippumattomat toiminnot `Promise.all`-metodilla sisäkkäisten kutsujen sijaan.
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` suoritetaan nykyisen operaation jälkeen ennen kuin tapahtumasilmukka siirtyy eteenpäin. `queueMicrotask` ajastaa standardin JavaScript-mikrotehtävän, vastaavasti kuin Promise-jatkokäsittelyt. `setImmediate` suoritetaan tapahtumasilmukan `check`-vaiheessa. `setTimeout(fn, 0)` suoritetaan `timers`-vaiheessa vähimmäisviiveen jälkeen. Yleensä `nextTick` suoritetaan ennen Promise-mikrotehtäviä, ja `setTimeout(0)`- sekä `setImmediate`-kutsujen suoritusjärjestys riippuu puolestaan kontekstista: I/O-takaisinkutsujen jälkeen `setImmediate` suoritetaan usein ensin.
Synkroninen koodi käyttää `try/catch`-rakennetta. Promise-pohjaisen koodin tulisi käyttää `await`-avainsanaa `try/catch`-lohkossa tai ketjuttaa `.catch`. Error-first -takaisinkutsujen tulisi tarkistaa virheargumentti ennen tuloksen käyttöä. Palvelinsovelluksissa tärkeitä ovat keskitetty virheiden käsittelyn väliohjelmisto (middleware), pyyntökontekstin lokitus, turvalliset viestit asiakkaalle sekä hallittu alasajo. `uncaughtException`-tapahtumaa ei tulisi käyttää korruptoituneen prosessin käynnissä pitämiseen; se on yleensä merkki siitä, että tulee tehdä lokikirjaus, vapauttaa resurssit ja käynnistää prosessi uudelleen.
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 Modules -standardin `import` eroavat toisistaan?
CommonJS käyttää `require`- ja `module.exports`-rakenteita, kun taas ES Modules käyttää `import`- ja `export`-rakenteita. ES Modules tukee staattista analyysiä ja tiedostotason `await`-avainsanaa (top-level await), ja ne valitaan käyttöön `package.json`-tiedoston `"type": "module"` -asetuksella tai `.mjs`-tiedostotunnisteella. CommonJS ladataan usein synkronisesti, ja se voidaan merkitä `.cjs`-tunnisteella. Nykyaikaisissa Node.js-projekteissa molemmat järjestelmät ovat olemassa, joten vahva vastaus välttää yliyksinkertaistamista ja selittää, että tiedostopääte, `package.json` ja projektin konteksti määrittävät käytettävän moduulitilan.
15Mitä ovat npm, package.json ja package-lock.json?
npm on pakettienhallintaohjelma ja CLI (Command Line Interface). package.json määrittelee skriptit, metatiedot, riippuvuudet ja devDependencies-riippuvuudet. package-lock.json tallentaa tarkan riippuvuuspuun, jotta asennukset ovat toistettavia. CI-ympäristöissä (Continuous Integration) suositaan yleensä npm ci -komentoa, koska se asentaa riippuvuudet tiukasti lukkotiedostosta ja epäonnistuu, jos package.json ja lukkotiedosto ovat ristiriidassa. Tämä suojaa koontiversioita tahattomilta muutoksilta riippuvuuksissa.
16Kuinka konfiguraatiota ja ympäristömuuttujia tulisi hallita?
Node.js lukee ympäristömuuttujia `process.env`-olion kautta, mutta niiden arvot ovat merkkijonoja, jotka tulisi validoida ja muuntaa oikeaan tyyppiin sovelluksen käynnistyessä. Salaisuuksia ei pitäisi tallentaa koodivarastoon, `.env`-tiedostot tulisi jättää versionhallinnan ulkopuolelle, ja tuotantoympäristön salaisuudet tulisi hakea ajoympäristön alustalta tai salaisuuksien hallintapalvelusta. Hyvin suunnitellut palvelut kaatuvat nopeasti, jos vaadittu konfiguraatio puuttuu. `NODE_ENV` on vakiintunut käytäntö, ei tietoturvaraja, joten koodin ei pidä luottaa siihen suojattuna mekanismina.
const port = Number(process.env.PORT ?? 3000);
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
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.
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')
);