T-CREATOR

Deno/Bun/Node のランタイムで共通動く Zod 環境のセットアップ

Deno/Bun/Node のランタイムで共通動く Zod 環境のセットアップ

JavaScript / TypeScript のランタイム環境は、今や Node.js だけではありません。Deno や Bun といった新しいランタイムが登場し、それぞれに特徴があるため、開発者は用途に応じて使い分けるようになってきました。

しかし、複数のランタイムで同じコードを動かそうとすると、パッケージ管理やモジュール解決の違いに悩まされることがあります。特に、型安全なバリデーションライブラリである Zod を使う場合、各ランタイムで適切に動作する環境をセットアップする必要があるのです。

本記事では、Deno、Bun、Node.js という 3 つの主要ランタイムで共通して動作する Zod 環境のセットアップ方法を、初心者の方にもわかりやすく解説いたします。

背景

JavaScript ランタイムの多様化

近年、JavaScript のランタイム環境が多様化しています。従来の Node.js に加えて、Deno や Bun といった新しいランタイムが登場し、それぞれが異なる哲学と特徴を持っています。

以下の図は、各ランタイムの特徴と位置づけを示しています。

mermaidflowchart TB
  runtime["JavaScript<br/>ランタイム"]
  nodejs["Node.js<br/>従来の標準"]
  deno["Deno<br/>セキュア&TypeScript"]
  bun["Bun<br/>高速&オールインワン"]

  runtime --> nodejs
  runtime --> deno
  runtime --> bun

  nodejs --> node_feature["・npm エコシステム<br/>・豊富なライブラリ<br/>・広範な採用実績"]
  deno --> deno_feature["・標準で TypeScript<br/>・セキュリティ重視<br/>・URL インポート"]
  bun --> bun_feature["・超高速起動<br/>・組み込みバンドラ<br/>・互換性重視"]

図で理解できる要点:

  • Node.js は npm エコシステムと豊富なライブラリが強み
  • Deno は TypeScript ネイティブとセキュリティが特徴
  • Bun は高速性とオールインワンツールとしての利便性が魅力

Zod とは

Zod は、TypeScript ファーストのスキーマバリデーションライブラリです。実行時のデータ検証と TypeScript の型推論を同時に実現できるため、型安全性を保ちながら API レスポンスやフォーム入力などを検証できます。

#項目説明
1型安全性TypeScript の型を自動推論
2軽量依存関係なしで動作
3柔軟性豊富なバリデーションメソッド
4エラー処理詳細なエラーメッセージ

ランタイム間の違い

各ランタイムは、モジュール解決やパッケージ管理の仕組みが異なります。

#ランタイムパッケージ管理モジュール解決TypeScript
1Node.jsnpm/yarnCommonJS/ESM要トランスパイル
2DenoURL/deps.tsESM のみネイティブ対応
3Bunnpm 互換ESM 優先ネイティブ対応

この違いを理解することで、なぜ共通環境のセットアップが必要なのかが見えてきます。

課題

ランタイム固有の依存関係管理

各ランタイムで Zod を使おうとすると、以下のような課題に直面します。

Node.js の場合:

  • npm または yarn でのインストールが必須
  • package.json の管理が必要
  • node_modules ディレクトリの肥大化

Deno の場合:

  • npm パッケージを直接利用できない(npm: 指定子が必要)
  • URL インポートの管理が煩雑
  • deps.ts ファイルでの依存関係集約が推奨される

Bun の場合:

  • npm 互換だが、一部のパッケージで互換性問題
  • 独自の bun.lockb ファイル形式
  • Node.js との微妙な動作差異

以下の図は、各ランタイムでの依存関係管理の流れを示しています。

mermaidflowchart LR
  subgraph nodejs_flow["Node.js"]
    npm1["yarn add zod"] --> pkg1["package.json"]
    pkg1 --> node_mod["node_modules/"]
    node_mod --> import1["import { z } from 'zod'"]
  end

  subgraph deno_flow["Deno"]
    url["URL指定"] --> deps["deps.ts"]
    deps --> import2["import { z } from './deps.ts'"]
  end

  subgraph bun_flow["Bun"]
    bun_add["bun add zod"] --> pkg2["package.json"]
    pkg2 --> bun_lock["bun.lockb"]
    bun_lock --> import3["import { z } from 'zod'"]
  end

図で理解できる要点:

  • Node.js は package.json と node_modules を中心とした管理
  • Deno は deps.ts ファイルで依存関係を集約
  • Bun は npm 互換だが独自のロックファイルを使用

モジュールシステムの不統一

JavaScript のモジュールシステムには、CommonJS と ES Modules(ESM)という 2 つの主要な方式があります。Zod は ESM で提供されているため、各ランタイムでの ESM サポート状況が重要になります。

発生しうるエラー例:

cssError [ERR_REQUIRE_ESM]: require() of ES Module not supported

このエラーは、CommonJS の require() で ESM パッケージを読み込もうとした際に発生します。

TypeScript の扱いの違い

Zod を TypeScript で使う場合、各ランタイムでの TypeScript サポートに違いがあります。

#ランタイムTypeScript サポート設定ファイルトランスパイル
1Node.js外部ツール必要tsconfig.json必須
2Denoネイティブサポートdeno.json(任意)不要
3Bunネイティブサポートtsconfig.json(任意)不要

Node.js では TypeScript のトランスパイルが必要ですが、Deno と Bun では不要です。この違いが、開発体験や環境構築の複雑さに影響を与えます。

解決策

共通化の基本方針

複数のランタイムで共通して動作する Zod 環境を構築するには、以下の方針が有効です。

#方針内容
1ESM を標準とするすべてのランタイムで ESM を使用
2TypeScript を活用型安全性を保ちつつ開発
3ランタイム判定を実装必要に応じて動作を切り替え
4共通のエントリポイントmain.ts などを共有

以下の図は、共通化のアプローチを示しています。

mermaidflowchart TD
  source["共通ソースコード<br/>(TypeScript)"]

  source --> entry["エントリポイント<br/>main.ts"]

  entry --> runtime_check{"ランタイム判定"}

  runtime_check -->|Node.js| node_run["Node.js で実行"]
  runtime_check -->|Deno| deno_run["Deno で実行"]
  runtime_check -->|Bun| bun_run["Bun で実行"]

  node_run --> output["同一の出力結果"]
  deno_run --> output
  bun_run --> output

図で理解できる要点:

  • 共通のソースコードから各ランタイムに分岐
  • ランタイム判定により適切な実行パスを選択
  • 最終的な出力結果は統一

プロジェクト構成

まず、共通で使えるプロジェクト構成を決めましょう。以下のようなディレクトリ構成を推奨します。

bashzod-multiruntime/
├── src/
│   ├── main.ts        # メインロジック
│   ├── schemas.ts     # Zod スキーマ定義
│   └── utils.ts       # ユーティリティ関数
├── deps.ts            # Deno 用依存関係
├── package.json       # Node.js/Bun 用
├── tsconfig.json      # TypeScript 設定
└── deno.json          # Deno 設定

この構成により、各ランタイムが必要とするファイルを適切に配置できます。

Zod のインストールと設定

それぞれのランタイムで Zod をセットアップする方法を見ていきましょう。

Node.js でのセットアップ

まず、package.json を作成します。ESM を使用するため、"type": "module" を指定することが重要です。

json{
  "name": "zod-multiruntime",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --loader ts-node/esm src/main.ts",
    "build": "tsc"
  },
  "dependencies": {
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "typescript": "^5.3.3",
    "ts-node": "^10.9.2"
  }
}

ポイント:

  • "type": "module" により ESM モードを有効化
  • ts-node で TypeScript を直接実行可能
  • zod は dependencies に配置

次に、Yarn でパッケージをインストールします。

bashyarn install

tsconfig.json の設定も重要です。以下のように設定しましょう。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

設定のポイント:

  • "module": "ES2022" で ESM を使用
  • "moduleResolution": "node" で Node.js の解決方式を指定
  • "strict": true で厳格な型チェックを有効化

Deno でのセットアップ

Deno では、deps.ts ファイルで依存関係を管理します。これにより、プロジェクト全体で使用するパッケージを一箇所に集約できます。

typescript// deps.ts
export { z } from 'npm:zod@^3.22.4';

ポイント:

  • npm: 指定子で npm パッケージを利用
  • バージョンを明示的に指定
  • export により再エクスポート

Deno の設定ファイル deno.json も作成します。

json{
  "tasks": {
    "dev": "deno run --allow-read --allow-env src/main.ts"
  },
  "compilerOptions": {
    "strict": true,
    "lib": ["deno.window"]
  },
  "imports": {
    "zod": "npm:zod@^3.22.4"
  }
}

設定のポイント:

  • tasks でスクリプトを定義
  • compilerOptions で TypeScript 設定
  • imports で依存関係のマッピング

Bun でのセットアップ

Bun は npm 互換なので、Node.js と同じ package.json を使用できます。ただし、Bun 専用のスクリプトを追加すると便利です。

json{
  "name": "zod-multiruntime",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "bun run src/main.ts",
    "dev:node": "node --loader ts-node/esm src/main.ts",
    "dev:deno": "deno run --allow-read --allow-env src/main.ts",
    "build": "bun build src/main.ts --outdir ./dist"
  },
  "dependencies": {
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "typescript": "^5.3.3"
  }
}

ポイント:

  • 各ランタイム用のスクリプトを用意
  • Bun は TypeScript をネイティブサポート
  • dev、dev で他のランタイムも実行可能

Bun でのインストールは以下のコマンドで実行します。

bashbun install

ランタイム判定の実装

各ランタイムで動作を切り替える必要がある場合、ランタイムを判定する関数を実装します。

まず、ユーティリティファイルを作成します。

typescript// src/utils.ts

/**
 * 現在のランタイムを判定する関数
 * グローバルオブジェクトの存在を確認して判定します
 */
export function detectRuntime(): string {
  // Deno の判定
  if (typeof Deno !== 'undefined') {
    return 'deno';
  }

  // Bun の判定
  if (typeof Bun !== 'undefined') {
    return 'bun';
  }

  // Node.js の判定
  if (
    typeof process !== 'undefined' &&
    process.versions?.node
  ) {
    return 'node';
  }

  return 'unknown';
}

この関数は、グローバルオブジェクトの存在をチェックすることで、どのランタイムで実行されているかを判定します。

次に、ランタイム情報を表示する関数も作成しましょう。

typescript// src/utils.ts に追加

/**
 * ランタイム情報を表示する関数
 */
export function printRuntimeInfo(): void {
  const runtime = detectRuntime();

  console.log(`実行環境: ${runtime}`);

  // 各ランタイム固有の情報を表示
  switch (runtime) {
    case 'deno':
      console.log(`Deno バージョン: ${Deno.version.deno}`);
      break;
    case 'bun':
      console.log(`Bun バージョン: ${Bun.version}`);
      break;
    case 'node':
      console.log(`Node.js バージョン: ${process.version}`);
      break;
  }
}

ポイント:

  • switch 文で各ランタイムの処理を分岐
  • バージョン情報も取得して表示
  • デバッグ時に有用

具体例

基本的な Zod スキーマの定義

それでは、実際に Zod を使ったバリデーションスキーマを定義してみましょう。

まず、スキーマ定義ファイルを作成します。

typescript// src/schemas.ts

import { z } from 'zod';

/**
 * ユーザー情報のスキーマ定義
 * 名前、メールアドレス、年齢を検証します
 */
export const userSchema = z.object({
  name: z.string().min(1, '名前は必須です'),
  email: z
    .string()
    .email('有効なメールアドレスを入力してください'),
  age: z
    .number()
    .int()
    .min(0, '年齢は0以上である必要があります')
    .max(150, '年齢は150以下である必要があります'),
});

このスキーマは、ユーザー情報を検証するためのものです。名前、メールアドレス、年齢の 3 つのフィールドを持ちます。

次に、型推論を活用します。Zod の強力な機能の一つは、スキーマから TypeScript の型を自動で生成できることです。

typescript// src/schemas.ts に追加

/**
 * userSchema から型を推論
 * これにより、TypeScript の型安全性を保てます
 */
export type User = z.infer<typeof userSchema>;

ポイント:

  • z.infer でスキーマから型を自動生成
  • スキーマと型定義の二重管理が不要
  • 型安全性を保ちながら開発可能

さらに、複雑なスキーマも定義してみましょう。

typescript// src/schemas.ts に追加

/**
 * 商品情報のスキーマ定義
 * オプショナルフィールドや配列も扱います
 */
export const productSchema = z.object({
  id: z.string().uuid('有効なUUIDを指定してください'),
  name: z.string().min(1, '商品名は必須です'),
  price: z
    .number()
    .positive('価格は正の数である必要があります'),
  description: z.string().optional(),
  tags: z.array(z.string()).default([]),
  inStock: z.boolean().default(true),
});

export type Product = z.infer<typeof productSchema>;

ポイント:

  • optional() でオプショナルフィールドを定義
  • default() でデフォルト値を設定
  • array() で配列型を扱う

以下の図は、スキーマとバリデーションの流れを示しています。

mermaidflowchart LR
  input["入力データ<br/>(JSON等)"]
  schema["Zod スキーマ<br/>(userSchema)"]
  validate{"バリデーション"}

  input --> validate
  schema --> validate

  validate -->|成功| typed["型付きデータ<br/>(User型)"]
  validate -->|失敗| error["ZodError<br/>詳細なエラー"]

  typed --> app["アプリケーション<br/>ロジック"]
  error --> handle["エラーハンドリング"]

図で理解できる要点:

  • 入力データとスキーマを照合してバリデーション
  • 成功時は型安全なデータとして扱える
  • 失敗時は詳細なエラー情報を取得

メインロジックの実装

それでは、実際にバリデーションを実行するメインロジックを実装します。

typescript// src/main.ts

import { userSchema, productSchema } from './schemas.ts';
import {
  printRuntimeInfo,
  detectRuntime,
} from './utils.ts';

/**
 * メイン関数
 * 各ランタイムで共通して動作します
 */
async function main() {
  // ランタイム情報を表示
  printRuntimeInfo();
  console.log('---');

  // ユーザーデータのバリデーション例
  await validateUserData();

  console.log('---');

  // 商品データのバリデーション例
  await validateProductData();
}

メイン関数では、ランタイム情報の表示と、2 つのバリデーション処理を実行します。

次に、ユーザーデータのバリデーション関数を実装します。

typescript// src/main.ts に追加

/**
 * ユーザーデータのバリデーション
 * 正常データと異常データの両方を検証します
 */
async function validateUserData() {
  console.log('ユーザーデータのバリデーション:');

  // 正常なデータ
  const validUser = {
    name: '山田太郎',
    email: 'yamada@example.com',
    age: 25,
  };

  try {
    const result = userSchema.parse(validUser);
    console.log('✓ バリデーション成功:', result);
  } catch (error) {
    console.error('✗ バリデーション失敗:', error);
  }
}

ポイント:

  • parse() メソッドでバリデーション実行
  • 成功時は検証済みデータを取得
  • 失敗時は try-catch でエラーをキャッチ

続いて、エラーケースも確認します。

typescript// src/main.ts の validateUserData 関数内に追加

// 異常なデータ(メールアドレスが不正)
const invalidUser = {
  name: '鈴木花子',
  email: 'invalid-email', // 不正なメールアドレス
  age: 30,
};

try {
  const result = userSchema.parse(invalidUser);
  console.log('✓ バリデーション成功:', result);
} catch (error) {
  // ZodError の詳細を表示
  if (error instanceof Error) {
    console.error('✗ バリデーション失敗:', error.message);
  }
}

Zod は、バリデーションに失敗すると詳細なエラーメッセージを提供します。これにより、どのフィールドがどのような理由で失敗したのかを正確に把握できるのです。

次に、商品データのバリデーション関数も実装しましょう。

typescript// src/main.ts に追加

/**
 * 商品データのバリデーション
 * オプショナルフィールドとデフォルト値の動作を確認します
 */
async function validateProductData() {
  console.log('商品データのバリデーション:');

  // description が省略されたデータ
  const product = {
    id: '123e4567-e89b-12d3-a456-426614174000',
    name: 'ノートパソコン',
    price: 89800,
    tags: ['電子機器', 'PC'],
  };

  try {
    const result = productSchema.parse(product);
    console.log('✓ バリデーション成功:', result);
    console.log(
      '  - inStock のデフォルト値:',
      result.inStock
    );
  } catch (error) {
    if (error instanceof Error) {
      console.error('✗ バリデーション失敗:', error.message);
    }
  }
}

ポイント:

  • オプショナルフィールドは省略可能
  • デフォルト値は自動で設定される
  • tags 配列も正しく検証される

最後に、メイン関数を実行します。

typescript// src/main.ts に追加

// メイン関数を実行
main().catch((error) => {
  console.error('予期しないエラーが発生しました:', error);
  const runtime = detectRuntime();

  // ランタイムに応じた終了処理
  if (runtime === 'deno') {
    Deno.exit(1);
  } else {
    process.exit(1);
  }
});

ポイント:

  • トップレベルで catch してエラーハンドリング
  • ランタイムに応じた終了処理を実装
  • エラー時は終了コード 1 を返す

実行方法

それでは、各ランタイムで実際に実行してみましょう。

Node.js での実行

Node.js で実行する場合は、以下のコマンドを使います。

bashyarn dev

または、直接実行する場合は以下のようにします。

bashnode --loader ts-node/esm src/main.ts

実行結果の例:

yaml実行環境: node
Node.js バージョン: v20.10.0
---
ユーザーデータのバリデーション:
 バリデーション成功: { name: '山田太郎', email: 'yamada@example.com', age: 25 }
 バリデーション失敗: [
  {
    "code": "invalid_string",
    "validation": "email",
    "path": ["email"],
    "message": "有効なメールアドレスを入力してください"
  }
]
---
商品データのバリデーション:
 バリデーション成功: {
  id: '123e4567-e89b-12d3-a456-426614174000',
  name: 'ノートパソコン',
  price: 89800,
  tags: [ '電子機器', 'PC' ],
  inStock: true
}
  - inStock のデフォルト値: true

Deno での実行

Deno で実行する場合は、以下のコマンドを使います。

bashdeno task dev

または、直接実行する場合は以下のようにします。

bashdeno run --allow-read --allow-env src/main.ts

ポイント:

  • --allow-read で読み取り権限を付与
  • --allow-env で環境変数アクセスを許可
  • Deno はデフォルトでセキュリティが厳格

実行結果:

yaml実行環境: deno
Deno バージョン: 1.39.0
---
ユーザーデータのバリデーション:
 バリデーション成功: { name: '山田太郎', email: 'yamada@example.com', age: 25 }
 バリデーション失敗: [バリデーションエラーの詳細]
---
商品データのバリデーション:
 バリデーション成功: [商品データの詳細]
  - inStock のデフォルト値: true

Bun での実行

Bun で実行する場合は、以下のコマンドを使います。

bashbun run dev

または、直接実行する場合は以下のようにします。

bashbun run src/main.ts

実行結果:

yaml実行環境: bun
Bun バージョン: 1.0.15
---
ユーザーデータのバリデーション:
 バリデーション成功: { name: '山田太郎', email: 'yamada@example.com', age: 25 }
 バリデーション失敗: [バリデーションエラーの詳細]
---
商品データのバリデーション:
 バリデーション成功: [商品データの詳細]
  - inStock のデフォルト値: true

エラーハンドリングの実装

Zod のエラーハンドリングをより詳細に行う方法を見てみましょう。

typescript// src/utils.ts に追加

import { ZodError } from 'zod';

/**
 * Zod のエラーをわかりやすく整形する関数
 * @param error ZodError オブジェクト
 * @returns 整形されたエラーメッセージ
 */
export function formatZodError(error: ZodError): string {
  const errors = error.errors.map((err) => {
    const path = err.path.join('.');
    return `  - ${path}: ${err.message}`;
  });

  return `バリデーションエラー:\n${errors.join('\n')}`;
}

この関数は、ZodError を人間が読みやすい形式に整形します。

次に、safeParse を使った安全なバリデーション方法も実装しましょう。

typescript// src/utils.ts に追加

/**
 * 安全にバリデーションを実行する関数
 * parse() と異なり、エラーを throw しません
 */
export function safeValidate<T>(
  schema: any,
  data: unknown
):
  | { success: true; data: T }
  | { success: false; error: string } {
  const result = schema.safeParse(data);

  if (result.success) {
    return { success: true, data: result.data };
  } else {
    return {
      success: false,
      error: formatZodError(result.error),
    };
  }
}

ポイント:

  • safeParse() は例外を投げない
  • 成功・失敗の両方を型安全に扱える
  • エラーハンドリングがシンプルになる

この関数を使った例を見てみましょう。

typescript// src/main.ts に追加例

import { safeValidate } from './utils.ts';

const result = safeValidate(userSchema, someData);

if (result.success) {
  console.log('データは正常です:', result.data);
} else {
  console.error('エラー:', result.error);
}

パフォーマンスの比較

各ランタイムでの実行速度を比較するベンチマークコードも作成できます。

typescript// src/benchmark.ts

import { userSchema } from './schemas.ts';
import { detectRuntime } from './utils.ts';

/**
 * バリデーションのベンチマークを実行
 * 1万回のバリデーションにかかる時間を計測します
 */
export function runBenchmark() {
  const runtime = detectRuntime();
  console.log(`${runtime} でのベンチマーク実行`);

  const iterations = 10000;
  const testData = {
    name: 'テストユーザー',
    email: 'test@example.com',
    age: 30,
  };

  const startTime = performance.now();

  for (let i = 0; i < iterations; i++) {
    userSchema.parse(testData);
  }

  const endTime = performance.now();
  const duration = endTime - startTime;

  console.log(
    `${iterations} 回のバリデーション: ${duration.toFixed(
      2
    )}ms`
  );
  console.log(
    `平均: ${(duration / iterations).toFixed(4)}ms/回`
  );
}

ポイント:

  • performance.now() で高精度な時間計測
  • 大量のバリデーションで性能差を確認
  • 各ランタイムの特性を把握できる

まとめ

本記事では、Deno、Bun、Node.js という 3 つの主要な JavaScript ランタイムで共通して動作する Zod 環境のセットアップ方法を解説いたしました。

各ランタイムには、パッケージ管理やモジュール解決の仕組みに違いがありますが、ESM を標準とし、適切な設定ファイルを用意することで、同一のコードベースを共有できることがお分かりいただけたかと思います。

特に重要なポイントは以下の通りです。

#ポイント詳細
1ESM の採用すべてのランタイムで ESM を使用することで統一性を確保
2TypeScript の活用型安全性を保ちながら開発できる環境を構築
3ランタイム判定必要に応じて各ランタイム固有の処理を実装
4共通のスキーマ定義Zod のスキーマは全ランタイムで共有可能
5適切なエラーハンドリングsafeParse や詳細なエラーメッセージを活用

Zod は、ランタイムに依存しない純粋な JavaScript/TypeScript ライブラリであるため、このような共通環境の構築が比較的容易です。これにより、開発者は特定のランタイムに縛られることなく、柔軟に環境を選択できるようになります。

今後、マルチランタイム対応のライブラリやツールはますます増えていくでしょう。本記事で紹介した手法は、Zod に限らず、他のライブラリでも応用できる考え方となっていますので、ぜひ参考にしてみてください。

型安全なバリデーションを各ランタイムで実現し、より堅牢なアプリケーション開発を進めていきましょう。

関連リンク