Bun入門:Nodeとの違いと、速い理由をbun install/test/serveで体感する
Bun入門。Nodeとの違い、bun install/run/test/buildの速さ、Bun.serveなど組み込みAPI、Node互換の実際、いつ選びいつ避けるか。コピペで動くAPIとテストで一気につかむ。
「npm install を待ってる時間、人生で何時間あるんだろう」
ある日、CIのログをぼんやり眺めていて気づきました。テスト本体は数秒で終わるのに、その前の依存インストールに毎回1分以上かかっている。ローカルでも同じで、ブランチを切り替えるたびに node_modules を入れ直して、コーヒーを淹れに行く。あの待ち時間、地味に積もります。
そんなときに bun install を試して、正直ちょっと笑いました。さっきまで1分待っていた同じプロジェクトが、数秒で終わったんです。
Bunは「速いNode.js互換ランタイム」とだけ紹介されがちです。でも、僕が実際に使って効いたのは速さだけじゃありません。Bunはランタイム(コードを動かす土台)であり、依存を入れるパッケージマネージャーであり、package.json のコマンドを動かすスクリプトランナーであり、テストを実行するテストランナーであり、バンドラーでもある。これが1本のCLIにまとまっている。道具箱ごと速くなった、という感覚に近いです。
この記事では、Nodeとの違いを押さえつつ、bun install / bun run / bun test / Bun.serve を、コピペで動く最小プロジェクトで一気に触っていきます。同じランタイム比較でも、権限モデル中心のDeno入門とは住み分けて、Bunは「速度と互換性」に絞ります。
この記事の要点
- Bunの一番の武器は速さ。とくに
bun installと起動が速く、CIとローカルの待ち時間がはっきり縮む。 - BunはNode互換を強く意識しているが**「完全に同じ」ではない**。ネイティブアドオンや一部の組み込みモジュールは要確認。
- TypeScriptがそのまま動く。
tscやts-nodeのビルド設定なしでbun src/server.tsが走る。 Bun.serve・bun testなど組み込みAPI/ツールが標準装備。Express無し・Jest無しでもサーバーとテストが書ける。- 選ぶ基準は単純。ローカル開発・テスト・小さなツールから入る。本番は互換性とデプロイ環境を確認してから。
Bunは何が違うのか:Nodeとの比較
ざっくり言うと、Nodeは「ランタイム単体」で、足りない道具(パッケージマネージャー、テスト、バンドラー)は別ツールを組み合わせます。Bunは「ランタイム+よく使う道具」が最初から一体です。
違いを表にすると、こんな感じです。
| 観点 | Node.js | Bun |
|---|---|---|
| パッケージ導入 | npm / yarn / pnpm を別途 | bun install が組み込み・高速 |
| TypeScript | tsconfig+ts-node等が必要 | .ts をそのまま実行 |
| テスト | Jest / Vitest を別途 | bun test が組み込み |
| HTTPサーバー | http か Express等 | Bun.serve が組み込み |
| バンドル | webpack / esbuild 等 | bun build が組み込み |
| 速度の体感 | 標準的 | install・起動が速い |
「全部入り」なので、最初のセットアップで悩む時間が短いのが効きます。とくにTypeScriptを書き始めるとき、Nodeだと「tsc? tsx? node --loader?」で詰まりがちですが、Bunは bun src/index.ts で終わりです。
ただし、ここで一つ釘を刺しておきます。Bunの互換性は高いですが、Nodeと100%同じではありません。後半の落とし穴で詳しく書きますが、「Nodeで動くものは全部そのまま動く」と思い込むと足をすくわれます。
なぜBunは速いのか
速さの理由を魔法だと思わないほうが、判断を間違えません。大きく3つあります。
ひとつ目は、実装言語です。Bunの本体はZigという低レベル言語で書かれていて、起動やファイル操作のオーバーヘッドが小さい。JavaScriptエンジンもV8ではなくJavaScriptCore(Safari系)を使っています。
ふたつ目は、パッケージインストールの作りです。bun install はキャッシュとリンクの戦略が効率的で、2回目以降がとくに速い。npm install でいつも待たされていた人ほど、差を体感しやすいところです。
みっつ目は、道具が一体なこと。ランタイム・テスト・バンドラーが別プロセスでなく一つにまとまっているので、起動コストや橋渡しのコストが減ります。
正直に書くと、すべての処理が常にNodeより速いわけではありません。重い計算そのものはエンジン依存ですし、ベンチマークは条件で変わります。でも、開発ループ(インストール→起動→テスト)の待ち時間という、毎日何十回も繰り返す部分が縮むのは、体験として大きいです。
まずは導入順を決める:全部移行しない
Bunを触るとき、最初にやってはいけないのが「既存の本番プロジェクトを一気に全部Bunへ」です。互換性の確認が済む前に本番を動かすのは、初めて乗る車でいきなり高速道路に入るようなものです。
僕がいつも使う導入順は、失敗しても戻せる順に4段階です。
| 段階 | 試すこと | 成功条件 |
|---|---|---|
| 1 | bun install で依存解決 | lockfileの差分を説明できる |
| 2 | bun run で既存scriptsを実行 | dev test lint の意味が変わらない |
| 3 | bun test で小さな単体テスト | watchやmockの差分を把握する |
| 4 | Bun.serve で小さなHTTP API | 本番移行前の制約を文章にできる |
Claude Codeに頼むときも、範囲を狭く指定するのがコツです。広く投げると、AIは気を利かせて本番設定まで触りに行きます。
既存のNode.jsプロジェクトを一度に移行しないで。まずBunで依存インストール、package scripts、単体テスト、ローカルHTTP APIだけ検証する。変更点と戻し方を最後に表で出して。
用語も先にそろえておくと、レビューが楽になります。
| 用語 | やさしい言い換え | レビューで見る点 |
|---|---|---|
| runtime | JavaScriptを動かす土台 | Node専用APIに依存していないか |
| package script | package.json に書く短縮コマンド | bun run で同じ意味になるか |
| test runner | テストを探して実行する係 | Jest固有機能を使いすぎていないか |
| Node compatibility | Node向けコードがどこまで動くか | 依存パッケージと組み込みAPIの差 |
コピペで試せる最小プロジェクト
ここからは手を動かします。既存アプリでいきなり実行せず、新しいフォルダで小さく試してください。BunのインストールはBun公式の導入手順に従えば1コマンドです。
まず初期化します。
mkdir bun-claude-lab
cd bun-claude-lab
bun init -y
package.json はBun専用にしすぎず、scriptsの名前から意味が読めるようにします。scriptsは「毎回長いコマンドを打たなくていいようにした短縮名」です。
{
"name": "bun-claude-lab",
"type": "module",
"scripts": {
"dev": "bun --watch src/server.ts",
"start": "bun src/server.ts",
"test": "bun test",
"check": "bun test && bun run scripts/runtime-check.ts"
},
"dependencies": {},
"devDependencies": {}
}
次に、Bunに組み込まれた Bun.serve でHTTPサーバーを作ります。Bun.serve は「Bun標準のHTTPサーバー起動API」です。Expressのような外部フレームワークは使わず、Request を受け取って Response を返すだけ。.ts のままビルドなしで動きます。
// src/server.ts
type ApiMessage = {
message: string;
receivedAt: string;
};
// JSONレスポンスを作る小さなヘルパー
function json(data: unknown, status = 200): Response {
return Response.json(data, {
status,
headers: { "Cache-Control": "no-store" }
});
}
function notFound(pathname: string): Response {
return json({ error: "not_found", pathname }, 404);
}
const server = Bun.serve({
port: Number(process.env.PORT ?? 3000),
async fetch(req) {
const url = new URL(req.url);
// 死活監視用のエンドポイント
if (url.pathname === "/health") {
return json({ ok: true, runtime: "bun" });
}
// POSTされたメッセージをそのまま返すエコーAPI
if (url.pathname === "/api/echo" && req.method === "POST") {
const body = (await req.json().catch(() => null)) as { message?: string } | null;
if (!body?.message) {
return json({ error: "message is required" }, 400);
}
const payload: ApiMessage = {
message: body.message,
receivedAt: new Date().toISOString()
};
return json(payload, 201);
}
return notFound(url.pathname);
}
});
console.log(`Listening on ${server.url}`);
起動します。
bun run dev
別のターミナルで動作を確認します。
curl http://localhost:3000/health
curl -X POST http://localhost:3000/api/echo \
-H "Content-Type: application/json" \
-d '{"message":"hello from Bun"}'
ここまでで、フレームワーク無し・ビルド無しでTypeScriptのAPIが立ち上がっています。Claude Codeに頼むなら、正常系だけでなく失敗時のレスポンスまで指定すると抜けが減ります。
src/server.tsに/api/echoを追加して。正常系、JSON不正、message欠落、未定義ルートを分けて実装し、curlで確認するコマンドも出して。
bun testで小さく固める
Bunには bun test(モジュール名は bun:test)が組み込まれていて、Jestに似た書き味でテストを書けます。Vitestを別途入れなくていいのが楽です。
まずロジックを切り出します。テストしやすい純粋な関数にしておくのがコツです。
// src/message.ts
export function normalizeMessage(input: string): string {
return input.trim().replace(/\s+/g, " ");
}
export function createReply(input: string): { message: string; length: number } {
const message = normalizeMessage(input);
if (message.length === 0) {
throw new Error("message must not be empty");
}
return { message, length: message.length };
}
テストは bun:test から describe expect test を読み込みます。Jestを知っていれば、ほぼそのままの感覚です。
// src/message.test.ts
import { describe, expect, test } from "bun:test";
import { createReply, normalizeMessage } from "./message";
describe("message helpers", () => {
test("余分な空白を1つにまとめる", () => {
expect(normalizeMessage(" hello bun ")).toBe("hello bun");
});
test("返信ペイロードを作る", () => {
expect(createReply(" Claude Code ")).toEqual({
message: "Claude Code",
length: 11
});
});
test("空文字は弾く", () => {
expect(() => createReply(" ")).toThrow("message must not be empty");
});
});
実行します。--watch を付けると保存のたびに走ります。
bun test
bun test --watch
ここで一つ注意です。bun test はJest互換を目指していますが、すべてのJest機能が実装済みとは限りません。mock・snapshot・fake timer あたりは差が出やすいので、既存のJestテストを移すときは「失敗=Bunのバグ」と決めつけず、テストがJest固有機能に依存していないか先に見てください。テストの考え方はClaude Codeテスト戦略も合わせて読むと整理しやすいです。
Claude Codeには観点を渡すと差分がレビューしやすくなります。
src/message.tsの単体テストをbun testで追加して。正常系、空文字、余分な空白、将来APIレスポンスに使う戻り値の形を確認して。Jest固有APIが必要なら、bun testで代替できるかコメントして。
bun runとbun buildを使い分ける
bun run は package.json の scripts を実行します。ここで一度はまるのが、Bun本体のコマンド名とscript名の衝突です。bun dev のような短縮形は、状況によってBunのサブコマンドと取り合いになることがあります。迷ったら短縮形ではなく、明示的に bun run dev を使うほうが事故りません。
既存プロジェクトでは、いきなり全scriptを書き換えず、まずNode版とBun版を並べた対応表を作ると安全です。
{
"scripts": {
"node:dev": "node --watch dist/server.js",
"node:test": "vitest run",
"bun:dev": "bun --watch src/server.ts",
"bun:test": "bun test",
"verify:bun": "bun run bun:test && bun run scripts/runtime-check.ts"
}
}
バンドルが必要なら bun build が使えます。フロント配布用に1ファイルへまとめたいときなどに、esbuildを別で入れずに済みます。
bun build src/server.ts --target=bun --outdir=dist
互換性をその場で確かめる小さなスクリプトも置いておくと、移行の判断材料になります。
// scripts/runtime-check.ts
import { existsSync } from "node:fs";
import { join } from "node:path";
const checks = [
["package.json exists", existsSync(join(process.cwd(), "package.json"))],
["Bun global is available", typeof Bun !== "undefined"],
["fetch is available", typeof fetch === "function"],
["Buffer is available", typeof Buffer !== "undefined"]
] as const;
for (const [label, ok] of checks) {
console.log(`${ok ? "PASS" : "FAIL"} ${label}`);
}
if (checks.some(([, ok]) => !ok)) {
process.exit(1);
}
実行します。
bun run verify:bun
この段階でClaude Codeに頼みたいのは、速度の宣伝ではなく差分の棚卸しです。
package.jsonのscriptsを読み、Bunで安全に試せるもの、Node.jsのまま残すもの、判断保留にするものを表にして。変更はまだしないで。理由と確認コマンドも書いて。
いつBunを選ぶか(と、避けたほうがいい場面)
ここが一番聞かれるところなので、正直に切り分けます。
Bunが向く場面:
- ローカル開発の高速化。 本番はNodeのままでも、
bun install・bun run・bun testだけBunにすると開発ループが短くなる。一番リスクが低くて効果が出やすい入り口です。 - 小さな社内ツールや管理画面のAPI。
Bun.serveだけで/health・JSON API・Webhook受信のPoCが作れる。依存が少ないので互換性の心配も小さい。 - TypeScriptの学習・検証プロジェクト。
.tsをそのまま動かせるので、ビルド設定で詰まらない。 - 記事・教材のデモサーバー。 コード例が手元で動くことが信頼につながる。軽いサンプルAPIをBunで作り、curlとテストをそろえると読者が試しやすい。
Bunを避ける(まだ早い)場面:
- ネイティブアドオンや特殊な組み込みモジュールに強く依存する本番。 後述の互換性の壁に当たりやすい。
- デプロイ先・監視・障害時の切り戻しが未整理な本番。 速くてもオペレーションが追いつかなければ事故ります。
- チームの足並みがそろっていないとき。 一部の人だけBun、一部はNode、だと「自分の環境では動く」が量産されます。
要するに、速さで本番を決めず、まずローカルとテストから。これだけで失敗の大半は避けられます。Node資産をそのまま使う発想は、権限モデルから入るDeno入門とも比べてみると、自分のチームにどちらが合うか見えてきます。
初心者がはまりやすい落とし穴
僕や周りが実際に踏んだ地雷を挙げます。
ひとつ目は、Node互換を「完全に同じ」と思い込むこと。Bunは互換性に力を入れていますが、node:vm、一部のストリーミング、古いCommonJS前提のパッケージ、postinstall に依存するパッケージなどは挙動が違うことがあります。依存が多い本番ほど、移行前に実際に動かして確認が要ります。
ふたつ目は、Jest前提のテストをそのまま bun test に流すこと。基本の expect や describe は近いですが、mockやsnapshot、DOM周りは差が出やすい領域です。
みっつ目は、scriptの意味が環境で変わること。bun run のフラグ位置、Bun内蔵コマンドとの名前衝突、Windowsでのshell挙動など、開発者ごとに差が出ます。チームで使うなら、README や CLAUDE.md に「使うコマンド」と「使わない短縮形」を書いておくと揉めません。
よっつ目は、速さだけで採用を決めること。インストールや起動が速くても、CI・Docker・デプロイ先・監視・切り戻しが未整理なら本番は早すぎます。
いつつ目は、Claude Codeに移行を丸投げすること。AIは差分作成には強い一方、実行環境の暗黙知やCIの秘密情報、運用の優先順位までは自動では分かりません。必ず「変更してよい範囲」「戻し方」「確認コマンド」「触らないファイル」を指定してください。速度改善そのものの考え方はClaude Codeパフォーマンス最適化にまとめています。
よくある質問
Q. BunはNode.jsを完全に置き換えられますか? A. 用途によります。小さなツールやローカル開発、テストならかなりの確率で置き換えられます。ネイティブアドオンや特殊な組み込みAPIに依存する大規模な本番は、互換性を一つずつ確認してからにしてください。
Q. 既存のnpmパッケージはそのまま使えますか?
A. 多くは bun install でそのまま入って動きます。ただし postinstall に強く依存するものや、Node固有の挙動を前提にしたものは差が出ることがあります。bun install 後に主要パスをテストで確認するのが安全です。
Q. bun run と bun の違いは何ですか?
A. bun run dev は package.json のscriptを実行します。bun src/index.ts はファイルを直接実行します。短縮形の bun dev はBunのサブコマンドと衝突することがあるので、scriptを呼ぶときは bun run を明示するのが無難です。
Q. TypeScriptの型チェックもBunがやってくれますか?
A. いいえ。Bunは .ts を実行しますが、型チェック(型エラーの検出)はしません。型を担保したいなら tsc --noEmit を別途CIに残してください。
Q. CIで使うときの注意点は? A. まずインストールとテストだけBunにするのが安全です。lockfileの扱い、キャッシュ設定、Dockerイメージの差を確認し、本番ビルドはNodeのまま並行運用してから段階的に寄せると失敗しにくいです。
実際に試した結果
この記事のサンプルは、Bun公式ドキュメントの Bun.serve・bun run・bun test・Node互換ページの考え方に合わせて、ローカルPoCとしてコピーしやすい最小構成にしました。手元で確かめるなら、bun run dev → curl /health → curl /api/echo → bun test → bun run verify:bun の順で進めると、HTTP API・scripts・テスト・互換性の最初の差分をきれいに切り分けられます。
僕自身の実感としては、Bunの価値は「全部置き換える」ことではなく、毎日の待ち時間を削るところにあります。冒頭の「npm install を待つ時間」を bun install に変えただけで、ブランチ切り替えのストレスが目に見えて減りました。まずはローカルとテストだけ。そこで互換性に問題がないと分かってから、少しずつ本番に近づける。この順番が、遠回りに見えていちばん安全で速い、というのが今の結論です。
基本コマンドを手元に固定したい人は、まず無料チートシートから。CLAUDE.md・hooks・権限・レビュー観点までまとめて整えるなら教材・テンプレート一覧、チームのNode資産をBunへ段階移行する判断やCI/CD、切り戻しまで一緒に設計したいならClaude Code研修・導入相談で実リポジトリ前提に整理できます。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。