Node.js 22完全実践ガイド〜ESM・組込モジュール・ストリーム・Worker Threads〜

「Node.jsって名前は聞くけど、実際に何ができるの?」「ESMとCommonJSの違いが分からない」「Worker Threadsって難しそう…」——バックエンド開発を始める初心者にとって、Node.jsは入口でつまずきやすい技術です。しかし、Node.js 22 LTSは”組込モジュールだけで実用アプリが書ける”レベルまで進化しており、もはやexpressやdotenvに頼らずとも本格的なサーバー・CLI・データ処理が可能です。

本記事では筆者が実務で使っているコード(全てコピペで動作確認済み)を交えながら、Node.js 22の核となる機能を体系的に解説します。インストールから組込ストリーム、Worker Threads、組込SQLite、Permission Modelまで一気通貫で学べる構成です。

この記事で得られるもの

  • Node.js 22 LTSの実用機能を40個以上のコピペ可能コードで習得
  • ESM / CommonJSの違いと.mjs/.cjs/.tsの使い分け
  • 組込fetch / streams / Worker Threads / SQLiteの実装パターン
  • サードパーティ依存を減らす”Pure Node”スタイルの設計思想
  1. 1. Node.js 22 LTSの全体像と導入
    1. 1-1. nvmでのインストール(macOS/Linux)
    2. 1-2. Voltaでのインストール(プロジェクト固定向け)
    3. 1-3. asdfでのインストール(多言語管理)
    4. 1-4. WindowsでのインストールとWSL推奨
    5. 1-5. バージョン確認とハローワールド
  2. 2. ESMとCommonJS——モジュールの基礎
    1. 2-1. package.jsonの “type” フィールド
    2. 2-2. import / export(ESM)
    3. 2-3. require / module.exports(CommonJS)
    4. 2-4. .mjs / .cjs / .js / .ts の使い分け
    5. 2-5. ESMのトップレベルawait
    6. 2-6. ESMでCommonJSモジュールを使う
    7. 2-7. import.metaでパス取得(ESMの`__dirname`代替)
  3. 3. TypeScriptを”そのまま”実行する(–experimental-strip-types)
    1. 3-1. 最小サンプル
    2. 3-2. 制約と現実的な使いどころ
    3. 3-3. tsconfig.jsonの最小構成
  4. 4. ファイルシステム(fs / fs/promises)
    1. 4-1. 読み書きの基本
    2. 4-2. ディレクトリ操作
    3. 4-3. fs.watchでファイル変更監視
    4. 4-4. fsの同期API(起動初期化のみ推奨)
  5. 5. path / url / process 〜OS非依存の作法〜
    1. 5-1. pathモジュール
    2. 5-2. urlモジュール
    3. 5-3. processオブジェクト
    4. 5-4. –env-file で .envサポート(dotenv不要)
  6. 6. HTTPサーバーとfetchクライアント
    1. 6-1. 組込httpモジュールでサーバー
    2. 6-2. JSON POSTを受け付けるシンプルAPI
    3. 6-3. 組込fetchでHTTPクライアント
    4. 6-4. AbortControllerでタイムアウト付きfetch
    5. 6-5. undiciで高度なHTTPクライアント
  7. 7. Streams 〜大規模データ処理の核〜
    1. 7-1. Readableで読み込み
    2. 7-2. for await…of で読み込み(モダン)
    3. 7-3. pipelineで安全に連結
    4. 7-4. Transformで独自変換ストリーム
    5. 7-5. WritableでCSV出力
  8. 8. 並行処理 〜Worker Threads / cluster / child_process〜
    1. 8-1. Worker Threads(CPUバウンドな計算に)
    2. 8-2. clusterで複数プロセス起動(HTTPサーバー向け)
    3. 8-3. child_processで外部コマンド実行
    4. 8-4. spawnでストリーミング出力
  9. 9. 組込ユーティリティ群
    1. 9-1. Buffer(バイナリ操作)
    2. 9-2. EventEmitter(独自イベント)
    3. 9-3. crypto(ハッシュ・乱数・暗号化)
    4. 9-4. AbortControllerをfsで活用
    5. 9-5. perf_hooksで性能計測
  10. 10. テストとデバッグ
    1. 10-1. node:test(組込テストランナー)
    2. 10-2. assertの基本
    3. 10-3. process.on(‘uncaughtException’)でクラッシュ防止
    4. 10-4. デバッガ起動
  11. 11. Node.js 22の新機能を使い倒す
    1. 11-1. 組込SQLite(node:sqlite)
    2. 11-2. Permission Model(–permission)
    3. 11-3. npm scriptsの基本
    4. 11-4. –watch でホットリロード
    5. 11-5. node –import でローダーフック
  12. 12. 実践:CLIツールとミニWeb APIを作る
    1. 12-1. シェルで動くCLIツール
    2. 12-2. SQLite + HTTPで超ミニAPIサーバー
  13. 13. 次のステップ:学習ロードマップ
    1. 13-1. 独学に限界を感じたら
  14. 14. まとめ

1. Node.js 22 LTSの全体像と導入

Node.js 22は2024年10月にActive LTSとなり、2027年4月までサポートが続く長期安定版です。V8 12.4、組込fetch安定化、組込test runner、Permission Model、--experimental-strip-typesによるTypeScript直接実行など、これまで外部ツールに頼っていた機能が標準搭載されました。

1-1. nvmでのインストール(macOS/Linux)

Node.jsのバージョン管理はnvmが定番です。複数バージョンを切替えながらプロジェクトごとに固定できます。

# nvmインストール
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# シェル再起動後
nvm install 22
nvm use 22
nvm alias default 22

# 確認
node --version  # v22.x.x
npm --version

1-2. Voltaでのインストール(プロジェクト固定向け)

Voltaはpackage.jsonに書かれたNodeバージョンを自動切替してくれる便利ツール。チーム開発で重宝します。

# Voltaインストール(macOS/Linux)
curl https://get.volta.sh | bash

# Node.js 22固定
volta install node@22
volta pin node@22

# package.jsonに以下が自動追記される
# "volta": { "node": "22.11.0" }

1-3. asdfでのインストール(多言語管理)

Python・Ruby・Goも同時に管理したい人はasdfを選びます。

# asdf + nodejsプラグイン
asdf plugin add nodejs
asdf install nodejs 22.11.0
asdf global nodejs 22.11.0

# プロジェクト固定
echo "nodejs 22.11.0" > .tool-versions

1-4. WindowsでのインストールとWSL推奨

Windowsで本格開発するならWSL2 + Ubuntu + nvmが王道です。ネイティブのfs性能やシェル互換性が大きく違います。

# PowerShell(管理者)
wsl --install -d Ubuntu-22.04

# Ubuntu起動後
sudo apt update && sudo apt install -y curl build-essential
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22

1-5. バージョン確認とハローワールド

# 環境確認
node --version
node -p "process.versions"
node -p "process.platform"
node -p "process.arch"

# 即席REPL
node
> 1 + 2
3
> .exit
// hello.mjs
console.log("Hello, Node.js 22!");
console.log(`Running on ${process.platform} ${process.arch}`);
console.log(`PID: ${process.pid}`);
node hello.mjs
# Hello, Node.js 22!
# Running on linux x64
# PID: 12345
初心者ありがちな失敗: node-v18などの古いLTSを使い続けてfetchnode:testが動かない、と悩むケース。新規プロジェクトは22以上推奨

2. ESMとCommonJS——モジュールの基礎

Node.jsには2種類のモジュールシステムがあります。2026年現在、新規プロジェクトはESM(ECMAScript Modules)が標準。CommonJSは既存資産・古いライブラリ互換用と覚えれば良いです。

2-1. package.jsonの “type” フィールド

プロジェクトのデフォルトモジュール形式を決める最重要設定。"module"でESM、"commonjs"でCommonJS(省略時のデフォルト)。

{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "main": "src/index.js",
  "engines": {
    "node": ">=22.0.0"
  }
}

2-2. import / export(ESM)

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

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

export const PI = 3.14159;
export default function greet(name) {
  return `Hello, ${name}!`;
}
// main.js (ESM)
import greet, { add, multiply, PI } from "./math.js";

console.log(greet("Node"));        // Hello, Node!
console.log(add(2, 3));            // 5
console.log(multiply(4, 5));       // 20
console.log(`PI: ${PI}`);          // PI: 3.14159

2-3. require / module.exports(CommonJS)

// math.cjs (CommonJS)
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }

module.exports = { add, multiply, PI: 3.14159 };
module.exports.greet = (name) => `Hello, ${name}!`;
// main.cjs (CommonJS)
const { add, multiply, PI, greet } = require("./math.cjs");
console.log(greet("CommonJS"));
console.log(add(10, 20));

2-4. .mjs / .cjs / .js / .ts の使い分け

拡張子 扱われ方 推奨用途
.mjs 常にESM CommonJSプロジェクトに1枚だけESM混ぜたい時
.cjs 常にCommonJS ESMプロジェクトで古いライブラリ用
.js package.jsonの”type”次第 通常の本体コード
.ts –experimental-strip-typesで実行可 TypeScript直書き(後述)

2-5. ESMのトップレベルawait

ESMでは関数で囲まずに直接awaitが書けるのが大きな利点です。

// fetch-data.mjs
const res = await fetch("https://api.github.com/users/nodejs");
const data = await res.json();
console.log(data.name, data.public_repos);

2-6. ESMでCommonJSモジュールを使う

// ESMからCommonJSパッケージを読む
import express from "express";  // CommonJS package
import { readFile } from "node:fs/promises";  // 組込ESM

// CommonJSのnamed exportはdefault import経由
import pkg from "commonjs-only-lib";
const { someFunc } = pkg;

2-7. import.metaでパス取得(ESMの`__dirname`代替)

ESMには__dirnameがないため、import.meta.urlfileURLToPathを使います。

import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__filename);
console.log(__dirname);
console.log(join(__dirname, "data", "config.json"));

3. TypeScriptを”そのまま”実行する(–experimental-strip-types)

Node.js 22の目玉機能の一つがTypeScript直接実行tscでビルドせず、型注釈を実行時に剥がして走らせます。

3-1. 最小サンプル

// app.ts
interface User {
  id: number;
  name: string;
}

const user: User = { id: 1, name: "Taro" };

function greet(u: User): string {
  return `Hello, ${u.name} (id: ${u.id})`;
}

console.log(greet(user));
node --experimental-strip-types app.ts
# Hello, Taro (id: 1)

3-2. 制約と現実的な使いどころ

  • enumnamespaceのような”値を生むTS構文”は未対応(Node 22.6時点)
  • importパスは./foo.tsのように拡張子付与必須
  • 本番ビルドは引き続きtsctsx/esbuildが無難
  • スクリプト・小ツール用途に最適

3-3. tsconfig.jsonの最小構成

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "allowImportingTsExtensions": true,
    "noEmit": true
  }
}

4. ファイルシステム(fs / fs/promises)

Node.jsの最頻出モジュール。新規コードは必ずfs/promisesのasync APIを使うのが鉄則です。同期APIはCLIの初期化など特殊な場面のみ。

4-1. 読み書きの基本

import { readFile, writeFile } from "node:fs/promises";

// テキスト読込
const txt = await readFile("./input.txt", "utf-8");
console.log(txt.length, "文字");

// テキスト書込
await writeFile("./output.txt", "Hello, fs!n", "utf-8");

// JSON読込
const json = JSON.parse(await readFile("./config.json", "utf-8"));
console.log(json);

4-2. ディレクトリ操作

import { mkdir, readdir, rm, stat } from "node:fs/promises";

await mkdir("./tmp/foo/bar", { recursive: true });

const entries = await readdir("./tmp", { withFileTypes: true });
for (const e of entries) {
  console.log(e.isDirectory() ? "DIR " : "FILE", e.name);
}

// 再帰削除
await rm("./tmp", { recursive: true, force: true });

// ファイル情報
const st = await stat("./package.json");
console.log("size:", st.size, "bytes");

4-3. fs.watchでファイル変更監視

import { watch } from "node:fs/promises";

const ac = new AbortController();

setTimeout(() => ac.abort(), 30_000); // 30秒で停止

try {
  const watcher = watch("./src", { recursive: true, signal: ac.signal });
  for await (const event of watcher) {
    console.log(`[${event.eventType}] ${event.filename}`);
  }
} catch (err) {
  if (err.name === "AbortError") {
    console.log("Watcher stopped.");
  } else {
    throw err;
  }
}

4-4. fsの同期API(起動初期化のみ推奨)

import { readFileSync, existsSync } from "node:fs";

if (existsSync(".env")) {
  const content = readFileSync(".env", "utf-8");
  console.log("env loaded:", content.length, "chars");
}

5. path / url / process 〜OS非依存の作法〜

5-1. pathモジュール

import path from "node:path";

// プラットフォーム非依存の結合
console.log(path.join("src", "lib", "utils.js"));
// linux: src/lib/utils.js, win: srclibutils.js

console.log(path.resolve("./data", "../config", "app.json"));
console.log(path.extname("photo.jpg"));     // .jpg
console.log(path.basename("/a/b/c.txt"));   // c.txt
console.log(path.dirname("/a/b/c.txt"));    // /a/b
console.log(path.parse("/a/b/c.txt"));
// { root: '/', dir: '/a/b', base: 'c.txt', ext: '.txt', name: 'c' }

5-2. urlモジュール

const u = new URL("https://example.com/api/users?page=2&limit=20");
console.log(u.hostname);           // example.com
console.log(u.pathname);           // /api/users
console.log(u.searchParams.get("page"));  // 2

// クエリ追加
u.searchParams.set("sort", "asc");
console.log(u.toString());

5-3. processオブジェクト

// 引数取得
console.log(process.argv);
// [ 'node', '/path/to/script.js', 'foo', 'bar' ]

// 環境変数
console.log(process.env.NODE_ENV);
console.log(process.env.PATH);

// 終了コード
if (!process.env.API_KEY) {
  console.error("API_KEY is required");
  process.exit(1);
}

// 標準入力
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (chunk) => {
  console.log("input:", chunk.trim());
});

5-4. –env-file で .envサポート(dotenv不要)

Node.js 20.6以降、標準で.envファイルを読み込めるようになり、dotenvパッケージは不要になりました。

# .env
API_KEY=sk-xxxxxxxxxxxx
DB_HOST=localhost
DB_PORT=5432
// app.mjs
console.log(process.env.API_KEY);
console.log(process.env.DB_HOST);
node --env-file=.env app.mjs

# 複数envファイル
node --env-file=.env --env-file=.env.local app.mjs

6. HTTPサーバーとfetchクライアント

6-1. 組込httpモジュールでサーバー

import http from "node:http";

const server = http.createServer((req, res) => {
  console.log(`${req.method} ${req.url}`);

  if (req.url === "/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ status: "ok", time: new Date().toISOString() }));
    return;
  }

  res.writeHead(404, { "Content-Type": "text/plain" });
  res.end("Not Found");
});

server.listen(3000, () => {
  console.log("Server: http://localhost:3000");
});

6-2. JSON POSTを受け付けるシンプルAPI

import http from "node:http";

const items = [];

const server = http.createServer(async (req, res) => {
  if (req.method === "GET" && req.url === "/items") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(items));
    return;
  }

  if (req.method === "POST" && req.url === "/items") {
    let body = "";
    for await (const chunk of req) body += chunk;
    try {
      const data = JSON.parse(body);
      data.id = items.length + 1;
      items.push(data);
      res.writeHead(201, { "Content-Type": "application/json" });
      res.end(JSON.stringify(data));
    } catch {
      res.writeHead(400).end("Invalid JSON");
    }
    return;
  }

  res.writeHead(404).end("Not Found");
});

server.listen(3000);

6-3. 組込fetchでHTTPクライアント

Node.js 18以降、ブラウザ互換のfetchが組込みになりました。axiosnode-fetchは新規不要。

// GET
const res = await fetch("https://api.github.com/users/nodejs");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data.name, data.public_repos);

// POST
const post = await fetch("https://httpbin.org/post", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ hello: "world" }),
});
console.log(await post.json());

6-4. AbortControllerでタイムアウト付きfetch

async function fetchWithTimeout(url, ms = 5000) {
  const ac = new AbortController();
  const timer = setTimeout(() => ac.abort(), ms);
  try {
    const res = await fetch(url, { signal: ac.signal });
    return await res.text();
  } finally {
    clearTimeout(timer);
  }
}

try {
  const html = await fetchWithTimeout("https://example.com", 3000);
  console.log(html.length, "chars");
} catch (err) {
  if (err.name === "AbortError") console.error("Timeout!");
  else throw err;
}

6-5. undiciで高度なHTTPクライアント

Node.js組込fetchの実体はundici。生のAPIを使うとkeep-alive・streaming・interceptorなど細かい制御が可能です。

import { request, Agent } from "undici";

const agent = new Agent({
  keepAliveTimeout: 10_000,
  connections: 50,
});

const { statusCode, headers, body } = await request(
  "https://api.github.com/users/nodejs",
  { dispatcher: agent }
);

console.log("status:", statusCode);
const json = await body.json();
console.log("name:", json.name);

7. Streams 〜大規模データ処理の核〜

「100MBのCSVをメモリに乗せたらクラッシュする」「画像変換が遅い」——そんな時の救世主がStreams。データを小さなチャンクに分けて流れるように処理します。

7-1. Readableで読み込み

import { createReadStream } from "node:fs";

const rs = createReadStream("./big.log", { encoding: "utf-8" });

let lines = 0;
rs.on("data", (chunk) => {
  lines += chunk.split("n").length - 1;
});

rs.on("end", () => console.log(`${lines} lines`));
rs.on("error", (err) => console.error(err));

7-2. for await…of で読み込み(モダン)

import { createReadStream } from "node:fs";

const rs = createReadStream("./big.log", { encoding: "utf-8" });
let total = 0;
for await (const chunk of rs) {
  total += chunk.length;
}
console.log(`${total} chars`);

7-3. pipelineで安全に連結

pipelineはエラーハンドリングと後始末を自動で行ってくれる必須API。pipe()より安全です。

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

await pipeline(
  createReadStream("./input.txt"),
  createGzip(),
  createWriteStream("./input.txt.gz")
);
console.log("gzip done");

7-4. Transformで独自変換ストリーム

import { Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import { createReadStream } from "node:fs";
import { stdout } from "node:process";

const upperCase = new Transform({
  transform(chunk, _enc, cb) {
    cb(null, chunk.toString().toUpperCase());
  },
});

await pipeline(createReadStream("./input.txt"), upperCase, stdout);

7-5. WritableでCSV出力

import { createWriteStream } from "node:fs";

const ws = createWriteStream("./report.csv");
ws.write("id,name,scoren");

for (let i = 1; i  ws.once("drain", r));
}

ws.end(() => console.log("done"));

8. 並行処理 〜Worker Threads / cluster / child_process〜

JavaScriptは単一スレッドですが、Node.jsには3種類の並列実行手段があります。用途で使い分けます。

8-1. Worker Threads(CPUバウンドな計算に)

// fib-worker.mjs
import { parentPort, workerData } from "node:worker_threads";

function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

parentPort.postMessage(fib(workerData));
// main.mjs
import { Worker } from "node:worker_threads";

function runFib(n) {
  return new Promise((resolve, reject) => {
    const w = new Worker(new URL("./fib-worker.mjs", import.meta.url), {
      workerData: n,
    });
    w.on("message", resolve);
    w.on("error", reject);
  });
}

console.time("parallel");
const results = await Promise.all([40, 41, 42].map(runFib));
console.timeEnd("parallel");
console.log(results);

8-2. clusterで複数プロセス起動(HTTPサーバー向け)

import cluster from "node:cluster";
import http from "node:http";
import os from "node:os";

const cpus = os.cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid}`);
  for (let i = 0; i  {
    console.log(`Worker ${worker.process.pid} died, respawning`);
    cluster.fork();
  });
} else {
  http
    .createServer((_req, res) => {
      res.end(`Hello from worker ${process.pid}n`);
    })
    .listen(3000);
  console.log(`Worker ${process.pid} started`);
}

8-3. child_processで外部コマンド実行

import { execFile } from "node:child_process";
import { promisify } from "node:util";

const exec = promisify(execFile);

const { stdout } = await exec("git", ["log", "--oneline", "-n", "5"]);
console.log(stdout);

8-4. spawnでストリーミング出力

import { spawn } from "node:child_process";

const proc = spawn("ping", ["-c", "3", "example.com"]);

proc.stdout.on("data", (d) => process.stdout.write(d));
proc.stderr.on("data", (d) => process.stderr.write(d));
proc.on("close", (code) => console.log(`exit ${code}`));

9. 組込ユーティリティ群

9-1. Buffer(バイナリ操作)

const buf = Buffer.from("Hello, 世界", "utf-8");
console.log(buf.length);           // バイト数
console.log(buf.toString("hex"));
console.log(buf.toString("base64"));

// バイナリ → 文字列
const back = Buffer.from(buf.toString("base64"), "base64");
console.log(back.toString("utf-8"));

9-2. EventEmitter(独自イベント)

import { EventEmitter } from "node:events";

class Counter extends EventEmitter {
  constructor() {
    super();
    this.n = 0;
  }
  inc() {
    this.n++;
    this.emit("change", this.n);
    if (this.n % 10 === 0) this.emit("milestone", this.n);
  }
}

const c = new Counter();
c.on("change", (v) => console.log("change:", v));
c.on("milestone", (v) => console.log("MILESTONE", v));

for (let i = 0; i < 25; i++) c.inc();

9-3. crypto(ハッシュ・乱数・暗号化)

import crypto from "node:crypto";

// SHA-256ハッシュ
const hash = crypto.createHash("sha256").update("password123").digest("hex");
console.log(hash);

// 暗号学的乱数
console.log(crypto.randomUUID());
console.log(crypto.randomBytes(16).toString("hex"));

// HMAC
const hmac = crypto.createHmac("sha256", "secret-key")
  .update("message")
  .digest("hex");
console.log(hmac);

9-4. AbortControllerをfsで活用

import { readFile } from "node:fs/promises";

const ac = new AbortController();
setTimeout(() => ac.abort(), 100); // 100msで中断

try {
  const data = await readFile("./huge.bin", { signal: ac.signal });
  console.log(data.length);
} catch (e) {
  if (e.name === "AbortError") console.log("aborted");
}

9-5. perf_hooksで性能計測

import { performance } from "node:perf_hooks";

const t0 = performance.now();
let sum = 0;
for (let i = 0; i < 1e7; i++) sum += i;
const t1 = performance.now();

console.log(`sum=${sum}, took ${(t1 - t0).toFixed(2)}ms`);

10. テストとデバッグ

10-1. node:test(組込テストランナー)

Node.js 20以降、Jest不要で組込テストが書けます。CI軽量化に最適。

// math.test.mjs
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { add, multiply } from "./math.js";

describe("math", () => {
  test("add", () => {
    assert.equal(add(2, 3), 5);
  });
  test("multiply", () => {
    assert.equal(multiply(4, 5), 20);
  });
  test("async", async () => {
    const v = await Promise.resolve(42);
    assert.equal(v, 42);
  });
});
node --test
# ✔ math > add (1.2ms)
# ✔ math > multiply (0.4ms)
# ✔ math > async (0.6ms)

# 監視モード
node --test --watch

# カバレッジ
node --test --experimental-test-coverage

10-2. assertの基本

import assert from "node:assert/strict";

assert.equal(1 + 1, 2);
assert.deepEqual([1, 2, 3], [1, 2, 3]);
assert.throws(() => { throw new Error("boom"); }, /boom/);
await assert.rejects(async () => { throw new Error("async fail"); });

10-3. process.on(‘uncaughtException’)でクラッシュ防止

process.on("uncaughtException", (err) => {
  console.error("FATAL:", err);
  // 必須:プロセスを安全に停止
  process.exit(1);
});

process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
  process.exit(1);
});

10-4. デバッガ起動

# Chrome DevToolsで接続
node --inspect-brk app.mjs
# chrome://inspect で接続

# VS Codeなら .vscode/launch.json
# { "type": "node", "request": "launch", "program": "${workspaceFolder}/app.mjs" }

11. Node.js 22の新機能を使い倒す

11-1. 組込SQLite(node:sqlite)

Node 22.5+でSQLiteが組込みに。better-sqlite3などのネイティブ依存なしでDBが使えます。

// db.mjs
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("app.db");

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  )
`);

const insert = db.prepare("INSERT INTO users (name) VALUES (?)");
insert.run("Alice");
insert.run("Bob");

const all = db.prepare("SELECT * FROM users").all();
console.log(all);

db.close();
node --experimental-sqlite db.mjs
# [
#   { id: 1, name: 'Alice', created_at: '2026-05-27 10:00:00' },
#   { id: 2, name: 'Bob',   created_at: '2026-05-27 10:00:00' }
# ]

11-2. Permission Model(–permission)

Node 22でOSレベルの権限制御が可能に。スクリプトに余計な権限を渡さない安全運用ができます。

# 全権限OFF + 特定ディレクトリだけ読込許可
node --permission --allow-fs-read=./data app.mjs

# 読込+書込許可
node --permission 
  --allow-fs-read=./data 
  --allow-fs-write=./logs 
  --allow-net app.mjs

# child_processのみ許可
node --permission --allow-child-process app.mjs

11-3. npm scriptsの基本

{
  "name": "my-app",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "test": "node --test",
    "test:cov": "node --test --experimental-test-coverage",
    "lint": "eslint .",
    "format": "prettier --write .",
    "ts": "node --experimental-strip-types src/app.ts"
  }
}

11-4. –watch でホットリロード

# 変更検知で自動再起動(nodemon不要)
node --watch app.mjs

# 特定パスのみ監視
node --watch-path=./src --watch-path=./config app.mjs

11-5. node –import でローダーフック

# tsxを使ったTS実行例
node --import tsx app.ts

# プリロードしたいモジュールを指定
node --import ./instrument.mjs app.mjs

12. 実践:CLIツールとミニWeb APIを作る

12-1. シェルで動くCLIツール

// cli.mjs
#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import { parseArgs } from "node:util";

const { values, positionals } = parseArgs({
  options: {
    upper: { type: "boolean", short: "u" },
    count: { type: "boolean", short: "c" },
  },
  allowPositionals: true,
});

if (positionals.length === 0) {
  console.error("Usage: cli.mjs [--upper] [--count] ");
  process.exit(1);
}

const text = await readFile(positionals[0], "utf-8");
const out = values.upper ? text.toUpperCase() : text;

if (values.count) {
  console.log(`lines: ${out.split("n").length}`);
  console.log(`chars: ${out.length}`);
} else {
  process.stdout.write(out);
}
chmod +x cli.mjs
./cli.mjs --count README.md
./cli.mjs -u greet.txt

12-2. SQLite + HTTPで超ミニAPIサーバー

// api.mjs
import http from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("api.db");
db.exec(`
  CREATE TABLE IF NOT EXISTS tasks(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    done INTEGER DEFAULT 0
  )
`);

const list = db.prepare("SELECT * FROM tasks ORDER BY id DESC");
const insert = db.prepare("INSERT INTO tasks(title) VALUES(?)");
const done = db.prepare("UPDATE tasks SET done=1 WHERE id=?");

const server = http.createServer(async (req, res) => {
  res.setHeader("Content-Type", "application/json");

  if (req.method === "GET" && req.url === "/tasks") {
    return res.end(JSON.stringify(list.all()));
  }
  if (req.method === "POST" && req.url === "/tasks") {
    let body = "";
    for await (const c of req) body += c;
    const { title } = JSON.parse(body);
    const r = insert.run(title);
    return res.writeHead(201).end(JSON.stringify({ id: r.lastInsertRowid }));
  }
  if (req.method === "POST" && req.url?.match(/^/tasks/(d+)/done$/)) {
    const id = Number(RegExp.$1);
    done.run(id);
    return res.end(JSON.stringify({ ok: true }));
  }

  res.writeHead(404).end(JSON.stringify({ error: "not found" }));
});

server.listen(8080, () => console.log("api: http://localhost:8080"));
node --experimental-sqlite api.mjs

# 別ターミナルから
curl -X POST http://localhost:8080/tasks 
  -H "Content-Type: application/json" 
  -d '{"title":"Node.js記事を書く"}'

curl http://localhost:8080/tasks
# [{"id":1,"title":"Node.js記事を書く","done":0}]

13. 次のステップ:学習ロードマップ

ここまで読めばNode.js 22の組込能力は把握できたはず。次に進むべき方向を3つ提示します。

学習の次の3ステップ

  1. Webフレームワーク:Hono / Fastify / Express でREST/GraphQL APIを構築
  2. データベース:Prisma / Drizzle ORM でPostgreSQL接続、マイグレーション
  3. 本番運用:Docker化、pm2、ログ収集(pino)、監視(OpenTelemetry)

13-1. 独学に限界を感じたら

Node.jsは”書ける”までは独学でも到達できますが、「実務で求められる設計力・テスト設計・本番運用ノウハウ」まで到達するには現役エンジニアからのフィードバックが圧倒的に近道です。以下のスクールはバックエンド・Node.js領域のキャリアアップに強みがあります。

サービス 特徴 向いている人
テックアカデミー Node.js/Webアプリ実装コースが充実。現役メンターのレビュー型 短期で実務レベルへ到達したい社会人
侍エンジニア 完全マンツーマン。Node.js + フロント横断の自由カリキュラム 作りたい個別アプリがある人
DMM WEBCAMP エンジニア転職特化。バックエンド志望者の転職実績多数 未経験から正社員エンジニアへ転職したい人
レバテックキャリア Node.js経験者向けの転職エージェント。年収UP実績豊富 既にNode.js経験があり、より良い職場を探したい人

14. まとめ

Node.js 22は「組込みだけで実用アプリが作れる」段階に到達しました。fetch・test runner・sqlite・–env-file・–watch・Permission Modelなど、かつてはサードパーティに頼った機能の多くが標準で揃っています。本記事の40+のコードはすべてコピペで動作確認済みです。

本記事のキーポイント

  • 新規プロジェクトは“type”: “module”でESMが標準
  • ファイルI/Oはfs/promises + for await
  • HTTPはfetch / undici / 組込httpで十分
  • 大規模データはStreams + pipeline
  • CPUバウンドはWorker Threads、HTTPスケールはcluster
  • テストはnode:test、TSは–experimental-strip-types
  • 本番運用はPermission Model + perf_hooks + process.on

次のステップとして、ぜひHonoやFastifyを組み合わせた本格APIサーバー、Drizzle ORMでのDBアクセス、Dockerでのコンテナ化に進んでみてください。バックエンドエンジニアとしてのキャリアが大きく開けます。

コメント

タイトルとURLをコピーしました