Node.jsバックエンド面接対策

Node.jsの面接問題と解答 2026

バックエンド開発者向けのNode.js面接問題を厳選したセットです。回答は口頭練習しやすい構成にしてあり、アーキテクチャを説明し、具体例を挙げ、運用上のトレードオフに触れられるようになっています。

Node.jsの模擬面接を開始クレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

アーキテクチャとイベントループ

1Node.jsとは何ですか?また、どのような用途で使用されますか?

Node.jsは、V8 JavaScriptエンジンを基盤とし、ネットワーク、ファイルシステム、プロセス、ストリーム、OSへのアクセス機能などのAPIによって拡張された、ブラウザ外で動作するJavaScript実行環境(ランタイム)です。主にREST API、GraphQLサービス、リアルタイムWebSocketサーバー、マイクロサービス、CLIツール、プロキシ、APIゲートウェイなどに広く利用されています。最大の強みはノンブロッキングI/Oであり、1つのプロセスで大量のネットワーク接続を効率的に処理できます。一方で、CPU負荷の高い長時間の演算を行うと、ワーカースレッドや別プロセス、あるいは別サービスに切り離さない限りイベントループをブロックしてしまうというトレードオフがあります。

AI コーチを使ってこの質問に答えてみる

2Node.js の内部動作の仕組みはどのようになっていますか?

JavaScriptはV8上で実行され、Node.jsのAPIは非同期処理をオペレーティングシステムまたはlibuvのスレッドプールに委任します。メインのJavaScriptスレッドはI/O操作の完了を待たず、他のコードの実行を継続します。処理が完了すると、そのコールバックやPromiseの継続処理がイベントループで実行可能な状態になります。このようにして、ユーザーのJavaScriptが通常1つのメインスレッド上で動作していても、Node.jsは高い並行性を実現しています。

AI コーチを使ってこの質問に答えてみる

3V8、libuv、およびイベントループとは何ですか?

V8はJavaScriptをコンパイルおよび実行し、JavaScriptオブジェクトとメモリを管理します。libuvはクロスプラットフォームなイベントループ、非同期I/Oの統合、および一部のファイルシステム操作、DNS、暗号処理などのためのワーカースレッドプールを提供します。イベントループは、完了した処理のコールバックを実行するタイミングを決定します。Node.js自体はJavaScript APIをネイティブ機能と結び付ける役割を果たしており、V8単体ではHTTPサーバー、ファイルシステムアクセス、プロセスAPIなどは提供されません。

AI コーチを使ってこの質問に答えてみる

4Node.jsがシングルスレッドと呼ばれることが多いのはなぜですか?

単一のNode.jsプロセスは、通常1つのメインスレッド上でユーザーのJavaScriptを実行するため、2つの通常のリクエストハンドラがそのスレッド内でまったく同時にJavaScriptを実行することはありません。しかし、プラットフォーム全体が文字通り1つのスレッドだけで構成されているわけではありません。libuvにはスレッドプールが存在し、OS (Operating System)が非同期I/Oを実行し、V8がガベージコレクションを行い、さらにNode.jsはworker_threadsや子プロセスもサポートしています。そのため、この表現は主にデフォルトのJavaScript実行モデルを指しています。

AI コーチを使ってこの質問に答えてみる

5Node.jsはどのようにして多数のリクエストを並行して処理できるのですか?

Node.jsはリクエストごとにスレッドをブロックするのではなく、I/O処理を開始した後にイベントループへと制御を戻します。たとえば、メインスレッドが新しい接続の受け付けや処理可能なコールバックの実行を継続している間に、1,000件のデータベース呼び出しを並行して処理(in-flight)できます。これはHTTP呼び出し、データベースクエリ、ファイル操作、ソケット通信などのI/Oバウンドなワークロードに適しています。一方で、リクエストハンドラで重い同期的なCPU処理を行うと、イベントループがブロックされて他のすべてのリクエストが遅延するため、パフォーマンスが大きく低下します。

AI コーチを使ってこの質問に答えてみる

6Node.jsのイベントループには、どのような主要フェーズがありますか?

Node.jsは、pending callbacks、idle/prepare、poll、check、close callbacksなどのlibuvのフェーズとタイマー処理を使用します。pollは多くのI/O(入出力)コールバックを、checkはsetImmediateを処理し、タイマーは最小待ち時間が経過してスケジュール上実行可能になるとsetTimeout/setIntervalを実行します。libuv 1.45およびNode.js 20以降では、各反復のpoll後にタイマーを処理し、互換性のためループに入る前にも最初のタイマー処理を行います。process.nextTickはNode独自のキューを使い、Promiseの反応とqueueMicrotaskはV8のマイクロタスクキューを使います。どちらもlibuvのフェーズではありません。これらはコールバック間のチェックポイントで処理されます。再帰的に追加し続けると、他の処理が実行機会を失うことがあります。

AI コーチを使ってこの質問に答えてみる

7同期、非同期、ノンブロッキングの処理はどう違いますか?

同期は制御の流れを表し、呼び出し元が続行する前に結果を返すか例外を送出します。非同期は、後からコールバック、Promise、その他の通知で結果を受け取る方式です。ブロッキングは、処理やリソースを待つ間、現在のスレッドが他の仕事をできないことです。ノンブロッキングのインターフェースは、例えば「実行するとブロックする」という結果を返すか通知を手配して、その待機を避けます。これらは別の観点です。同期だからといってI/O(入出力)の準備を待つとは限らず、Promiseを返すだけで重い同期計算がノンブロッキングになるわけでもありません。Node.jsのfs.readFileSyncは同期かつブロッキングです。fs.promises.readFileは非同期に結果を返し、その間メインスレッドは他の処理を行えます。リクエスト処理では適切な非同期I/Oインターフェースを優先します。

import fs from 'node:fs';

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

const asyncData = await fs.promises.readFile(
  'file.txt',
  'utf8'
);
AI コーチを使ってこの質問に答えてみる

8イベントループのスタベーションとは何ですか?

同期JavaScriptやチェックポイントのキューへの絶え間ない追加によって、I/O(入出力)コールバック、タイマー、リクエストを適時に処理できなくなる状態です。重い計算、大きなJSON処理、再帰的なマイクロタスクの登録、process.nextTickの過剰使用などが原因になります。入力サイズを制限し、重い処理をプロファイリングして、計算負荷の高い仕事をworker_threadsや別サービスへ移します。処理を分割する場合はsetImmediateなどでイベントループの後続の周回へ実際に制御を譲ります。解決済みPromiseへのawaitやqueueMicrotaskを繰り返すだけでは、I/Oが依然として実行機会を失う場合があります。頻繁に通るリクエスト処理では、同期のブロッキングインターフェースを避けます。

app.get('/report', (req, res) => {
  const result = performVeryHeavyCalculation();
  res.json(result);
});
AI コーチを使ってこの質問に答えてみる

非同期プログラミングとエラー

9コールバックとコールバック地獄とは何ですか?

コールバックは、別の処理から呼び出してもらうために渡す関数です。Array.mapのように同期実行される場合も、fs.readFileのように非同期実行される場合もあります。従来のNode.jsの非同期コールバックでは、callback(error, result)のように最初の引数をエラーとする慣習がよく使われます。コールバック地獄は、依存するコールバックが深く入れ子になり、制御の流れやエラー処理が分かりにくくなる状態です。名前付き関数、処理の細分化、Promise、async/awaitで読みやすくできます。独立した処理は、同時実行数の制限とエラー処理を考慮し、必要に応じてPromise.allなどで一緒に開始します。

import fs from 'node:fs';

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

  console.log(data);
});
AI コーチを使ってこの質問に答えてみる

10Promise とは何ですか?また、async/await はどのように動作しますか?

Promise は非同期処理の将来的な結果を表すオブジェクトであり、pending(保留中)、fulfilled(成功)、rejected(拒否)のいずれかの状態を取ります。async 関数は常に Promise を返します。await は対象の Promise が確定(settle)するまで現在の async 関数の実行のみを一時停止し、Node.js プロセス全体をブロックすることはありません。優れた回答では、try/catch によるエラーハンドリング、未処理のまま放置された Promise(floating Promise)の回避、そして独立した処理を不必要に直列実行させないための Promise.all の活用に言及することが望まれます。

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()
]);
AI コーチを使ってこの質問に答えてみる

11process.nextTick、queueMicrotask、setImmediate、setTimeoutはどう違いますか?

process.nextTickはNodeのnext-tickキューへ、queueMicrotaskはPromiseの反応にも使われるV8のマイクロタスクキューへ処理を登録します。CommonJSのトップレベルの通常のチェックポイントでは、nextTickが登録済みのPromise/queueMicrotaskより先に実行されます。ESモジュールのトップレベル評価は既にマイクロタスクの文脈で行われるため、登録済みマイクロタスクがnextTickより先に実行される場合があります。これは文脈依存で、実行中のJavaScriptを割り込ませる普遍的な優先規則ではありません。setImmediateはcheckで実行されます。setTimeout(fn, 0)も最小待ち時間を必要とし、即時実行ではありません。両者の順序は登録場所によりますが、同じI/O(入出力)コールバック内で登録した場合はsetImmediateが先です。nextTick固有の動作が不要なら、移植性のあるマイクロタスクへの延期にはqueueMicrotaskを優先します。

// 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.
AI コーチを使ってこの質問に答えてみる

12Node.jsではエラーをどのように処理すべきですか?

同期的な例外にはtry/catchを使います。Promiseベースの処理ではtry/catch内でawaitするか、拒否を処理するハンドラーを付けます。外側の同期try/catchでは、後で実行されるコールバックの例外は捕捉できません。エラーを第1引数に取るコールバックではその引数を確認し、EventEmitterのerrorイベントには適切なハンドラーが必要です。サーバーではリクエストのエラー処理を集約し、有用な文脈をログに記録して、内部情報をクライアントへ漏らさないようにします。uncaughtException後に安全な継続を前提としてはいけません。必要最小限の同期的な緊急クリーンアップだけを行い、プロセスを終了して監視プロセスに再起動させます。通常の終了には計画的で穏当なシャットダウンが有効ですが、未捕捉例外後の任意の非同期復旧は信頼できる戦略ではありません。

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;
});
AI コーチを使ってこの質問に答えてみる

モジュールとプロジェクト管理

13Node.jsにおけるモジュールとは何ですか?

モジュールとは、明示的にエクスポートされたインターフェイスを持つ独立したコードの構成単位です。Node.jsは、`node:fs` や `node:http` などの組み込みモジュール、ローカルプロジェクトのモジュール、npmパッケージ、CommonJSモジュール、ECMAScriptモジュールをサポートしています。モジュールを利用することで責務の境界が明確になり、コードの再利用が可能になります。本番環境におけるトレードオフは結合度です。モジュール間で過度に深くインポートし合ったり、インポート時に副作用を伴う処理を隠蔽していたりすると、テストや起動時の挙動の把握が難しくなります。

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

// app.js
import { add } from './math.js';
AI コーチを使ってこの質問に答えてみる

14CommonJSのrequireとESモジュールのimportはどう違いますか?

CommonJSはrequireとmodule.exports、ESモジュールはimportとexportを使います。requireは通常同期的に読み込み、条件付きで呼び出せます。静的なimport宣言は依存関係グラフを解析可能にし、動的なimport()はPromiseを返します。現在のNode.jsは対応する相互運用規則に従い、トップレベルawaitのないESモジュールのグラフもrequireで読み込めるため、ESモジュールが常に非同期評価を意味するわけではありません。.cjs/.mjs、package.jsonのtype、適用される構文検出規則がモジュール形式を決めます。ESモジュールはトップレベルawaitをサポートします。CommonJSは__dirname/__filenameを提供し、現在のNode.jsはファイルベースのESモジュールにimport.meta.dirname/filenameを提供します。import.meta.urlはURLベースの解決や旧バージョン対応の方法に役立ちます。

// 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.
AI コーチを使ってこの質問に答えてみる

15npm、package.json、package-lock.jsonとは何ですか?

npmはパッケージ管理用のコマンドラインインターフェースです。package.jsonはメタデータ、スクリプト、直接依存するパッケージの要件を記述し、要件にはバージョン範囲をよく使います。package-lock.jsonは解決済みの依存関係ツリー、パッケージのバージョン、整合性情報を記録します。アプリケーションのロックファイルはバージョン管理に含めます。npm ciは継続的インテグレーションで有用です。ロックファイルからインストールし、既存のnode_modulesを削除し、package.jsonとの不一致があれば失敗します。ロックファイルは依存関係の意図しない変動を減らしますが、OS、ネイティブビルド、ライフサイクルスクリプト、Nodeのバージョン、インストール設定まで同一にはしません。再現可能なビルドには、ツールチェーンと環境の管理も必要です。

{
  "name": "api",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "test": "node --test"
  },
  "dependencies": {},
  "devDependencies": {}
}
AI コーチを使ってこの質問に答えてみる

16設定と環境変数はどのように管理すべきですか?

環境変数はprocess.envで読み取ります。存在する値は文字列で、存在しないプロパティはundefinedです。起動時に必須値を検証し、意図的に型変換します。PORTをNumberで変換するだけでは、空文字列、NaN、範囲外のポートを拒否できません。必須設定が不正なら明確なエラーで停止します。実際の秘密情報はバージョン管理に含めず、本番の秘密情報はデプロイ基盤や秘密情報管理サービスから渡します。秘密情報を含まない設定例は保存できます。NODE_ENVはアプリケーション上の慣習であり、信頼できるセキュリティ境界ではありません。

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');
AI コーチを使ってこの質問に答えてみる

イベント、バッファ、ストリーム

17EventEmitterとは何ですか?

EventEmitterは、Node.jsプロセス内でパブリッシュ・サブスクライブパターンを実装する仕組みです。onはリスナーを登録し、onceは1回限りのリスナーを登録し、offはリスナーを削除し、emitはリスナーを登録順に同期的に呼び出します。この同期的性質は重要であり、低速なリスナーがあるとemitの処理全体がブロックされます。errorイベントは特別であり、ハンドリングされていないerrorイベントが発生するとプロセスがクラッシュする可能性があります。長期間稼働するシステムでは、メモリリークを防ぐために不要になったリスナーを削除することも不可欠です。

import { EventEmitter } from 'node:events';

const emitter = new EventEmitter();

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

emitter.emit('orderCreated', { id: 42 });
AI コーチを使ってこの質問に答えてみる

18Bufferは何のために使用されますか?

Bufferは固定サイズのバイト列を表します。ファイル処理、TCPソケット、HTTPボディ、画像データ、暗号処理、およびバイナリプロトコルなどで使用されます。`Buffer.from` は既存のデータからバッファを作成し、`Buffer.alloc` はゼロで初期化されたメモリを割り当てます。一方、`Buffer.allocUnsafe` は高速ですが、上書きされるまで古いメモリデータが含まれている可能性があります。そのため、`allocUnsafe` はコードが直ちにバッファをデータで満たし、古いデータを外部に一切露出させない場合にのみ適しています。

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.
AI コーチを使ってこの質問に答えてみる

19ストリームとは何ですか?また、どのような種類が存在しますか?

ストリームは、すべてのデータをメモリに読み込むのではなく、データをチャンク単位で処理する仕組みです。主な種類には `Readable`、`Writable`、`Duplex`、`Transform` があります。ファイル、HTTPリクエストおよびレスポンスオブジェクト、TCPソケット、gzipストリームなどが一般的な例です。ストリームはメモリ使用量とレイテンシを削減できるため、大容量ファイルやネットワークトラフィックの処理において価値があります。本番環境向けの回答としては、パイプラインチェーン全体でストリームのエラーを適切に処理する必要がある点にも触れるべきです。

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;
}
AI コーチを使ってこの質問に答えてみる

20バックプレッシャーとは何ですか?また、なぜpipelineが役立つのですか?

バックプレッシャーとは、データの送信元(ソース)が宛先(デスティネーション)の消費速度を上回るペースでデータを生成するときに発生する現象です。バックプレッシャーを制御しないと、プロセスが低速化したりクラッシュしたりするまでメモリが増大する可能性があります。Streamとpipeでもデータフローを協調させることは可能ですが、node:stream/promisesのpipelineはエラーを適切に伝播させ、関連するストリームを閉じ、Promiseを返すため、複数ステップの処理チェーンにおいてより安全です。ファイルの圧縮、アップロード、ダウンロード、およびストリーム変換の標準的な選択肢として適しています。

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')
);
AI コーチを使ってこの質問に答えてみる