Node.js / Bun コピペで使える実践 Tips 集【中級者向け】

Node.js / Bun コピペで即使えるコードを、現場でよく使うトピック別にまとめました。Node.js と Bun の両対応コードを中心に、Bun 固有の高速 API も併記します。

ファイル操作(fs/promises と Bun.file)

テキストファイルの読み書き

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

const content = await readFile("input.txt", "utf-8");
await writeFile("output.txt", content.toUpperCase());
// Bun
const file = Bun.file("input.txt");
const content = await file.text();
await Bun.write("output.txt", content.toUpperCase());

ディレクトリ内のファイル一覧を再帰取得

import { readdir } from "node:fs/promises";
import { join } from "node:path";

async function walk(dir: string): Promise<string[]> {
  const entries = await readdir(dir, { withFileTypes: true });
  const files = await Promise.all(
    entries.map((e) =>
      e.isDirectory() ? walk(join(dir, e.name)) : [join(dir, e.name)]
    )
  );
  return files.flat();
}

const allFiles = await walk("./src");

ファイルの存在確認

// Node.js
import { access } from "node:fs/promises";
import { constants } from "node:fs";

async function exists(path: string): Promise<boolean> {
  try {
    await access(path, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}
// Bun
const exists = await Bun.file("config.json").exists();

ストリーム処理(大きなファイルの読み書き)

大容量 CSV をチャンクで読み込む(Node.js)

import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";

async function processLargeCSV(path: string) {
  const stream = createReadStream(path, { encoding: "utf-8" });
  const rl = createInterface({ input: stream, crlfDelay: Infinity });

  let lineCount = 0;
  for await (const line of rl) {
    lineCount++;
    const cols = line.split(",");
    // 1行ずつ処理
    if (lineCount % 10_000 === 0) {
      console.log(`processed ${lineCount} lines`);
    }
  }
}

await processLargeCSV("data.csv");

ストリームをパイプして変換(Node.js)

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

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

await pipeline(
  createReadStream("input.txt"),
  upper,
  createGzip(),
  createWriteStream("output.txt.gz")
);

Bun でストリームを読む

const file = Bun.file("large.json");
const stream = file.stream();
const reader = stream.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // value は Uint8Array
  console.log(value.byteLength);
}

子プロセス実行(execa / Bun.spawn)

execa でコマンド実行(Node.js)

import { execa } from "execa";

// 結果を文字列で受け取る
const { stdout } = await execa("git", ["log", "--oneline", "-5"]);
console.log(stdout);

// 失敗時に例外
try {
  await execa("npm", ["test"]);
} catch (err) {
  if (err instanceof Error && "exitCode" in err) {
    console.error("exit code:", (err as { exitCode: number }).exitCode);
  }
}

ストリームで出力を受け取る(execa)

import { execa } from "execa";

const proc = execa("npm", ["run", "build"], { stdout: "pipe" });
proc.stdout?.on("data", (chunk: Buffer) => {
  process.stdout.write(chunk);
});
await proc;

Bun.spawn でコマンド実行

const proc = Bun.spawn(["git", "log", "--oneline", "-5"], {
  stdout: "pipe",
});

const output = await new Response(proc.stdout).text();
await proc.exited;
console.log(output);

Bun.spawnSync(同期版)

const result = Bun.spawnSync(["git", "rev-parse", "HEAD"]);
const hash = result.stdout.toString().trim();
console.log(hash);

環境変数管理(dotenv / Bun の組み込みサポート)

Node.js + dotenv

// npm i dotenv
import "dotenv/config";

const port = Number(process.env.PORT ?? 3000);
const dbUrl = process.env.DATABASE_URL;

if (!dbUrl) {
  throw new Error("DATABASE_URL is required");
}

型安全な環境変数ユーティリティ

function requireEnv(key: string): string {
  const value = process.env[key];
  if (!value) throw new Error(`Missing env var: ${key}`);
  return value;
}

function optionalEnv(key: string, fallback: string): string {
  return process.env[key] ?? fallback;
}

const config = {
  databaseUrl: requireEnv("DATABASE_URL"),
  port: Number(optionalEnv("PORT", "3000")),
  nodeEnv: optionalEnv("NODE_ENV", "development"),
} as const;

Bun は .env を自動ロード

// Bun は .env, .env.local, .env.production などを自動で読む
// dotenv パッケージ不要

const port = Number(Bun.env.PORT ?? 3000);
const secret = Bun.env.JWT_SECRET;

if (!secret) {
  throw new Error("JWT_SECRET is required");
}

HTTP サーバーの最小構成(Bun.serve)

ルーターなしの基本サーバー

const server = Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);

    if (url.pathname === "/health") {
      return new Response("OK", { status: 200 });
    }

    if (url.pathname === "/api/echo" && req.method === "POST") {
      return new Response(req.body, {
        headers: { "Content-Type": "application/json" },
      });
    }

    return new Response("Not Found", { status: 404 });
  },
});

console.log(`Listening on http://localhost:${server.port}`);

JSON API サーバー(型付き)

type User = { id: number; name: string };

const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

function json(data: unknown, status = 200): Response {
  return new Response(JSON.stringify(data), {
    status,
    headers: { "Content-Type": "application/json" },
  });
}

Bun.serve({
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url);

    if (req.method === "GET" && url.pathname === "/users") {
      return json(users);
    }

    if (req.method === "POST" && url.pathname === "/users") {
      const body = (await req.json()) as Partial<User>;
      if (!body.name) return json({ error: "name required" }, 400);
      const user: User = { id: users.length + 1, name: body.name };
      users.push(user);
      return json(user, 201);
    }

    return json({ error: "Not Found" }, 404);
  },
});

Node.js の最小 HTTP サーバー(標準ライブラリ)

import { createServer } from "node:http";

const server = createServer(async (req, res) => {
  if (req.url === "/health") {
    res.writeHead(200).end("OK");
    return;
  }

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

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

Worker Threads で並列処理

メインスレッドからワーカーを起動(Node.js)

// worker.ts
import { parentPort, workerData } from "node:worker_threads";

function heavyCalc(n: number): number {
  let result = 0;
  for (let i = 0; i < n; i++) result += Math.sqrt(i);
  return result;
}

parentPort?.postMessage(heavyCalc(workerData as number));
// main.ts
import { Worker } from "node:worker_threads";
import { cpus } from "node:os";

function runWorker(n: number): Promise<number> {
  return new Promise((resolve, reject) => {
    const w = new Worker(new URL("./worker.ts", import.meta.url), {
      workerData: n,
    });
    w.on("message", resolve);
    w.on("error", reject);
  });
}

const cores = cpus().length;
const tasks = Array.from({ length: cores }, (_, i) => runWorker(1_000_000 * (i + 1)));
const results = await Promise.all(tasks);
console.log(results);

ワーカープールで再利用

import { Worker } from "node:worker_threads";

class WorkerPool {
  private workers: Worker[] = [];
  private queue: Array<{ resolve: (v: number) => void; data: number }> = [];

  constructor(private size: number, private scriptUrl: URL) {
    for (let i = 0; i < size; i++) this.spawn();
  }

  private spawn() {
    const w = new Worker(this.scriptUrl, { workerData: 0 });
    w.on("message", (result: number) => {
      const next = this.queue.shift();
      if (next) {
        next.resolve(result);
        w.postMessage(next.data);
      } else {
        this.workers.push(w);
      }
    });
    this.workers.push(w);
  }

  run(data: number): Promise<number> {
    return new Promise((resolve) => {
      const w = this.workers.pop();
      if (w) {
        w.once("message", resolve);
        w.postMessage(data);
      } else {
        this.queue.push({ resolve, data });
      }
    });
  }
}

Bun でワーカー起動

// Bun は Web Worker API を使う
const worker = new Worker(new URL("./worker.ts", import.meta.url));

worker.postMessage({ n: 1_000_000 });

worker.onmessage = (e: MessageEvent<number>) => {
  console.log("result:", e.data);
  worker.terminate();
};

まとめ

各 Tips をランタイムとカテゴリ別に整理します。

カテゴリNode.jsBun
ファイル読み書きfs/promisesreadFile / writeFileBun.file().text() / Bun.write()
ファイル存在確認fs.access + try/catchBun.file().exists()
ストリーム処理readline / stream.pipelineWeb Streams API(file.stream()
子プロセスexeca(npm パッケージ)Bun.spawn / Bun.spawnSync
環境変数dotenv/config + process.env自動ロード、Bun.env
HTTP サーバーnode:httpcreateServerBun.serve(fetch ベース)
並列処理worker_threads + WorkerWeb Worker API

Bun は Node.js 互換モードを持つため、node:fsnode:http の多くは Bun でもそのまま動きます。新規プロジェクトでは Bun ネイティブ API を使うと起動速度・スループットの恩恵を最大限に受けられます。既存の Node.js コードベースへの段階的な移行にも役立ててください。