Node.js-haastatteluun valmistautuminen

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.

Aloita Node.js-harjoitushaastatteluEi vaadi luottokorttia. 1 ilmainen sessio saatavilla.
Teknisten haastattelujen harjoittelu englanniksiTila, jossa ei-äidinkieliset puhujat voivat harjoitella teknisen haastattelun läpäisemistä.

Arkkitehtuuri ja tapahtumasilmukka

1Mikä on Node.js ja mihin sitä käytetään?

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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

2Miten Node.js toimii sisäisesti?

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ä.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

3Mitä ovat V8, libuv ja tapahtumasilmukka?

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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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ä.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

import fs from 'node:fs';

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

const asyncData = await fs.promises.readFile(
  'file.txt',
  'utf8'
);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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ä.

app.get('/report', (req, res) => {
  const result = performVeryHeavyCalculation();
  res.json(result);
});
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Asynkroninen ohjelmointi ja virheet

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);
});
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

10Mikä on Promise ja miten async/await toimii?

`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.

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()
]);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

12Miten virheitä tulisi käsitellä Node.js:ssä?

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.

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;
});
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Moduulit ja projektinhallinta

13Mitä moduulit ovat Node.js:ssä?

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';
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

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

// ES Modules
import fs from 'node:fs';
export { calculate };
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

{
  "name": "api",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "test": "node --test"
  },
  "dependencies": {},
  "devDependencies": {}
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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');
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Tapahtumat, puskurit ja tietovirrat

17Mikä on `EventEmitter`?

`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 });
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

18Mihin `Buffer`-luokkaa käytetään?

`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);
console.log(buffer.toString('utf8'));

const safe = Buffer.alloc(1024);
const fast = Buffer.allocUnsafe(1024);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

import fs from 'node:fs';

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

input.pipe(output);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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')
);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa