Node.js入門|サーバーサイドJavaScriptの基本と実践ガイド

kento_morota 21分で読めます

Node.jsは、JavaScriptをサーバーサイドで実行できるランタイム環境です。フロントエンドでJavaScriptを使っている開発者にとって、同じ言語でバックエンドも開発できるという大きなメリットがあります。

本記事では、Node.jsの基本概念、環境構築、モジュールシステム、非同期処理、ファイル操作、そして簡単なHTTPサーバーの構築まで、初心者が押さえるべき基礎を網羅的に解説します。

Node.jsとは何か

Node.jsは、GoogleのV8 JavaScriptエンジン上に構築されたサーバーサイドのJavaScriptランタイムです。2009年にRyan Dahlによって開発され、現在ではWebアプリケーション、CLI ツール、IoTなど幅広い分野で利用されています。

Node.jsの特徴

シングルスレッド+イベントループ:Node.jsはシングルスレッドで動作しますが、イベントループと非同期I/Oにより、多数の同時接続を効率的に処理できます。ファイル読み込みやDB問い合わせなどのI/O処理中にスレッドがブロックされないため、少ないリソースで高い並行性を実現します。

ノンブロッキングI/O:従来のサーバー(ApacheやPHPなど)はリクエストごとにスレッドを割り当てますが、Node.jsはI/O待ちの間に他のリクエストを処理するノンブロッキングモデルを採用しています。

npm/pnpmエコシステム:世界最大のパッケージレジストリであるnpmを通じて、数百万のパッケージを利用できます。

フルスタックJavaScript:フロントエンドとバックエンドで同じ言語を使えるため、チームの学習コストが下がり、コードの共有も容易です。

Node.jsが得意な領域

Node.jsは以下の用途に特に適しています。

APIサーバー(REST/GraphQL)、リアルタイムアプリケーション(チャット、通知)、マイクロサービス、CLIツール、SSR(サーバーサイドレンダリング)、ビルドツール・開発ツール。

一方、CPU集約型の処理(画像処理、複雑な計算)はシングルスレッドの制約により得意ではありませんが、Worker Threadsを使えば対応可能です。

環境構築とプロジェクト初期化

Node.jsのインストール

Node.jsのバージョン管理には、voltaやnvmの使用を推奨します。プロジェクトごとに異なるバージョンを切り替えられます。

# voltaでのインストール(推奨)
curl https://get.volta.sh | bash
volta install node@22

# バージョン確認
node -v   # v22.x.x
npm -v    # 10.x.x

# pnpmのインストール(推奨パッケージマネージャー)
volta install pnpm

プロジェクトの初期化

# プロジェクトディレクトリ作成
mkdir my-node-app && cd my-node-app

# package.json の生成
pnpm init

# TypeScriptのセットアップ(推奨)
pnpm add -D typescript @types/node tsx
npx tsc --init
// package.json
{
  "name": "my-node-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.7.0"
  }
}
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

モジュールシステム

Node.jsには2つのモジュールシステムがあります。従来のCommonJS(require/module.exports)と、現在主流のESモジュール(import/export)です。

ESモジュール(推奨)

// src/utils/math.ts - モジュールのエクスポート
export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export const PI = 3.14159;

// デフォルトエクスポート
export default class Calculator {
  private result = 0;

  add(n: number): this {
    this.result += n;
    return this;
  }

  getResult(): number {
    return this.result;
  }
}
// src/index.ts - モジュールのインポート
import Calculator, { add, multiply, PI } from "./utils/math.js";

console.log(add(1, 2));       // 3
console.log(multiply(3, 4));   // 12
console.log(PI);               // 3.14159

const calc = new Calculator();
console.log(calc.add(10).add(20).getResult()); // 30

Node.js組み込みモジュール

// node: プレフィックス付きで組み込みモジュールをインポート
import path from "node:path";
import { fileURLToPath } from "node:url";
import os from "node:os";

// __dirname の代替(ESモジュールでは直接使えない)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

console.log("現在のファイル:", __filename);
console.log("現在のディレクトリ:", __dirname);
console.log("OS:", os.platform());
console.log("CPUコア数:", os.cpus().length);
console.log("空きメモリ:", Math.floor(os.freemem() / 1024 / 1024), "MB");

非同期処理の基本

Node.jsの最大の特徴は非同期処理です。I/O操作(ファイル読み書き、ネットワーク通信、DB問い合わせ)は非同期で実行され、完了を待たずに次の処理へ進みます。

async/await

// 非同期関数の基本
async function fetchData(url: string): Promise<unknown> {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("データ取得に失敗:", error);
    throw error;
  }
}

// 複数の非同期処理を並列実行
async function fetchMultiple() {
  const urls = [
    "https://api.example.com/users",
    "https://api.example.com/products",
    "https://api.example.com/orders",
  ];

  // Promise.all で並列実行(すべて成功が必要)
  const results = await Promise.all(
    urls.map((url) => fetchData(url))
  );

  // Promise.allSettled で並列実行(一部失敗してもOK)
  const settled = await Promise.allSettled(
    urls.map((url) => fetchData(url))
  );

  settled.forEach((result, i) => {
    if (result.status === "fulfilled") {
      console.log(`${urls[i]}: 成功`);
    } else {
      console.log(`${urls[i]}: 失敗 - ${result.reason}`);
    }
  });
}

イベントループの理解

// イベントループの実行順序を理解する
console.log("1: 同期処理");

setTimeout(() => {
  console.log("2: setTimeout(マクロタスク)");
}, 0);

Promise.resolve().then(() => {
  console.log("3: Promise.then(マイクロタスク)");
});

process.nextTick(() => {
  console.log("4: process.nextTick");
});

console.log("5: 同期処理");

// 出力順序:
// 1: 同期処理
// 5: 同期処理
// 4: process.nextTick
// 3: Promise.then(マイクロタスク)
// 2: setTimeout(マクロタスク)

同期処理が最初に実行され、その後process.nextTick、Promiseのマイクロタスク、setTimeoutのマクロタスクの順で処理されます。

ファイル操作

Node.jsのfsモジュール(のPromises API)を使ったファイル操作の基本を見ていきます。

ファイルの読み書き

import fs from "node:fs/promises";
import path from "node:path";

// ファイル読み込み
async function readConfig(filePath: string): Promise<Record<string, unknown>> {
  try {
    const content = await fs.readFile(filePath, "utf-8");
    return JSON.parse(content);
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") {
      console.log("ファイルが見つかりません。デフォルト設定を使用します。");
      return {};
    }
    throw error;
  }
}

// ファイル書き込み
async function writeLog(message: string): Promise<void> {
  const logDir = path.join(process.cwd(), "logs");
  const logFile = path.join(logDir, "app.log");

  // ディレクトリがなければ作成
  await fs.mkdir(logDir, { recursive: true });

  const timestamp = new Date().toISOString();
  const logEntry = `[${timestamp}] ${message}\n`;

  // 追記モード
  await fs.appendFile(logFile, logEntry, "utf-8");
}

// ディレクトリ内のファイル一覧
async function listFiles(dirPath: string): Promise<string[]> {
  const entries = await fs.readdir(dirPath, { withFileTypes: true });

  const files: string[] = [];
  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isFile()) {
      files.push(fullPath);
    } else if (entry.isDirectory()) {
      // 再帰的にサブディレクトリも探索
      const subFiles = await listFiles(fullPath);
      files.push(...subFiles);
    }
  }
  return files;
}

ストリームによる大容量ファイル処理

import { createReadStream, createWriteStream } from "node:fs";
import { createInterface } from "node:readline";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

// 大容量ファイルを1行ずつ処理
async function processLargeFile(filePath: string): Promise<void> {
  const stream = createReadStream(filePath, "utf-8");
  const rl = createInterface({ input: stream });

  let lineCount = 0;
  for await (const line of rl) {
    lineCount++;
    // 1行ずつ処理(メモリに全体を読み込まない)
    if (line.includes("ERROR")) {
      console.log(`Line ${lineCount}: ${line}`);
    }
  }
  console.log(`Total lines: ${lineCount}`);
}

// ファイルをgzip圧縮
async function compressFile(input: string, output: string): Promise<void> {
  await pipeline(
    createReadStream(input),
    createGzip(),
    createWriteStream(output)
  );
  console.log(`Compressed: ${input} -> ${output}`);
}

HTTPサーバーの構築

標準ライブラリでの基本サーバー

import http from "node:http";

interface RouteHandler {
  (req: http.IncomingMessage, res: http.ServerResponse): void;
}

const routes: Record<string, Record<string, RouteHandler>> = {
  GET: {
    "/": (req, res) => {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ message: "Hello, Node.js!" }));
    },
    "/api/health": (req, res) => {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({
        status: "ok",
        uptime: process.uptime(),
        memory: process.memoryUsage(),
      }));
    },
  },
  POST: {
    "/api/echo": (req, res) => {
      let body = "";
      req.on("data", (chunk) => (body += chunk));
      req.on("end", () => {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ echo: JSON.parse(body) }));
      });
    },
  },
};

const server = http.createServer((req, res) => {
  const method = req.method || "GET";
  const url = req.url || "/";
  const handler = routes[method]?.[url];

  if (handler) {
    handler(req, res);
  } else {
    res.writeHead(404, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "Not Found" }));
  }
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

環境変数の管理

// Node.js 22以降は --env-file フラグで .env を読み込み可能
// node --env-file=.env src/index.ts

// 環境変数へのアクセス
const config = {
  port: parseInt(process.env.PORT || "3000", 10),
  nodeEnv: process.env.NODE_ENV || "development",
  dbUrl: process.env.DATABASE_URL || "",
};

if (!config.dbUrl) {
  console.error("DATABASE_URL is required");
  process.exit(1);
}

エラーハンドリングとプロセス管理

グローバルなエラーハンドリング

// キャッチされなかったPromiseエラー
process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection:", reason);
  // ログ記録後にプロセスを安全に終了
});

// キャッチされなかった例外
process.on("uncaughtException", (error) => {
  console.error("Uncaught Exception:", error);
  // クリーンアップ処理後にプロセスを終了
  process.exit(1);
});

// シグナルハンドリング(Graceful Shutdown)
function gracefulShutdown(signal: string) {
  console.log(`${signal} received. Shutting down gracefully...`);

  server.close(() => {
    console.log("HTTP server closed");
    // DB接続のクローズなど
    process.exit(0);
  });

  // 10秒以内にクローズできなければ強制終了
  setTimeout(() => {
    console.error("Force shutdown");
    process.exit(1);
  }, 10000);
}

process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));

カスタムエラークラス

class AppError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public code: string = "INTERNAL_ERROR"
  ) {
    super(message);
    this.name = "AppError";
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource}が見つかりません`, 404, "NOT_FOUND");
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public errors: Record<string, string[]> = {}
  ) {
    super(message, 400, "VALIDATION_ERROR");
  }
}

// 使用例
function getUser(id: number) {
  const user = users.find((u) => u.id === id);
  if (!user) {
    throw new NotFoundError("ユーザー");
  }
  return user;
}

まとめ

Node.jsは、JavaScriptの知識をそのまま活かしてサーバーサイド開発ができる強力なランタイムです。

基本概念:シングルスレッド+イベントループによるノンブロッキングI/Oが最大の特徴です。I/O多重度の高いアプリケーション(API、リアルタイム通信)に特に適しています。

モジュールシステム:ESモジュール(import/export)が現在の標準です。package.jsonに"type": "module"を指定し、node:プレフィックスで組み込みモジュールをインポートします。

非同期処理:async/awaitで直感的に記述でき、Promise.allやPromise.allSettledで並列処理も容易です。

ファイル操作:fs/promisesのAPI で非同期にファイルを読み書きし、大容量ファイルはストリームで効率的に処理します。

HTTPサーバー:標準のhttpモジュールでサーバーが構築でき、実務ではExpressやFastifyなどのフレームワークを組み合わせて使います。

次のステップとして、Express.jsを使ったREST API構築や、データベースとの連携を学んでいくとよいでしょう。

#Node.js#JavaScript#サーバーサイド
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう