この記事の対象読者
- JavaScriptまたはTypeScriptの基本文法を理解している方
- Node.jsを使った経験がある、または名前を聞いたことがある方
- 「なんか新しいランタイムがあるらしい」と気になっている方
この記事で得られること
- Denoがなぜ生まれたのか、その背景と歴史の理解
- Denoの特徴とNode.jsとの違いの把握
- Denoで実際にプロジェクトを作成し、コードを動かせるスキル
- Node.jsプロジェクトをDenoで動かす方法
この記事で扱わないこと
- JavaScriptの基礎文法
- Node.jsの詳細な使い方
- Deno Deployなどのクラウドサービス
1. Denoとの出会い
「このNode.jsプロジェクト、依存関係多すぎない...?」
node_modulesフォルダを開いて、その中身の量に絶句した経験はありませんか?
私は何度もあります。
たった1つのライブラリを入れただけなのに、いつの間にか数百のパッケージがインストールされている。
「これ、本当に全部必要なの?」という疑問を抱えながら開発を続けていました。
ある日、Denoという新しいランタイムの存在を知りました。
驚いたのは、その開発者がNode.jsと同じ人物だったことです。
Ryan Dahl氏。Node.jsを2009年に作り、JavaScriptをサーバーサイドに持ち込んだ張本人。
その彼が「Node.jsには後悔がある」と語り、Denoを作ったというのです。
「え、自分で作ったものを否定するの?」と最初は驚きました。
しかし、その理由を知れば知るほど、納得せざるを得ませんでした。
Denoは「Node.jsのアナグラム」です。
No-deをひっくり返してDe-no。
これは単なる遊び心ではなく、「Node.jsの反省を活かした新しいランタイム」という意志の表れです。
ここまでで、Denoが「ただの新しいツール」ではないことが伝わったでしょうか。
次は、この記事で使う用語を整理しておきましょう。
2. 前提知識の確認
本題に入る前に、この記事で登場する用語を確認します。
2.1 ランタイムとは
プログラムを実行するための環境のことです。
JavaScriptはブラウザで動く言語でしたが、ランタイムがあればブラウザの外でも動きます。
Node.jsやDenoがサーバーサイドJavaScriptのランタイムです。
2.2 V8エンジンとは
GoogleがChrome用に開発したJavaScript実行エンジンです。
非常に高速で、Node.jsもDenoもこのV8を採用しています。
エンジンは同じでも、その「外側」の設計が異なるのがポイントです。
2.3 TypeScriptとは
JavaScriptに「型」を追加した言語です。
変数や関数の引数に型を指定することで、バグを事前に防げます。
Node.jsでは別途コンパイルが必要ですが、Denoはそのまま実行できます。
2.4 ESモジュール(ESM)とは
JavaScriptの公式モジュールシステムです。
import/export構文を使ってコードを分割・再利用できます。
Node.jsが採用していたCommonJS(require/module.exports)の後継です。
2.5 npmとは
Node Package Managerの略で、JavaScriptのパッケージ管理システムです。
250万以上のパッケージが公開されている、世界最大級のレジストリです。
これらの用語が押さえられたら、いよいよDenoの歴史を見ていきましょう。
3. Denoが生まれた背景
3.1 Ryan Dahlの「10の後悔」
2018年6月、JSConf EUでRyan Dahl氏は衝撃的なプレゼンを行いました。
タイトルは「10 Things I Regret About Node.js」。
Node.jsの生みの親が、自らの創造物に対する後悔を公開したのです。
彼が挙げた主な後悔は以下の通りです。
| 後悔 | 内容 |
|---|---|
| Promiseを採用しなかった | 非同期処理がコールバック地獄になった |
| セキュリティを軽視した | ファイルやネットワークに無制限アクセス可能 |
| GYPビルドシステム | 複雑でメンテナンスが困難 |
| package.json | 不必要に複雑な依存管理 |
| node_modules | 深いネスト構造と肥大化 |
| 拡張子の省略 |
require('module')で.jsを省略可能にした |
| index.js | 暗黙的なエントリーポイント |
3.2 Node.jsの設計上の問題
Node.jsは2009年に生まれました。
当時はJavaScriptの標準モジュールシステム(ESM)が存在せず、CommonJSを採用しました。
また、サーバーサイドでの利用を前提に、ファイルシステムやネットワークへのアクセスを自由にしました。
これらの決定は当時としては合理的でした。
しかし、15年経った今、以下の問題が顕在化しています。
セキュリティの問題: あなたがインストールした何気ないnpmパッケージ。
そのパッケージ(と、その依存先の依存先の...)は、あなたのファイルを読み放題、ネットワークにアクセスし放題です。
依存関係の爆発: 1つのパッケージを入れると、数百の依存がついてくることも珍しくありません。
有名な話として、is-odd(奇数判定)がis-numberに依存し、それがさらに別のパッケージに依存する...という連鎖があります。
3.3 Denoの誕生
このプレゼンの最後に、Ryan Dahl氏は新しいプロジェクトを発表しました。
それがDenoです。
Denoは以下の設計思想で作られています。
- セキュアバイデフォルト: 明示的に許可しない限り、何もできない
- TypeScriptネイティブ: 設定なしでTypeScriptを直接実行
- Web標準API: ブラウザと同じAPIを使える
- 単一バイナリ: 依存関係なしで動く単一の実行ファイル
2020年5月13日、Deno 1.0が正式リリースされました。
そして2024年10月9日、待望のDeno 2.0がリリース。
Node.jsとnpmとの完全な後方互換性が実現しました。
背景がわかったところで、Denoの具体的な特徴を見ていきましょう。
4. Denoの基本概念
4.1 セキュアバイデフォルト
Denoの最大の特徴は「デフォルトで何もできない」ことです。
Node.jsでは、実行したスクリプトはあらゆるファイルを読み書きし、ネットワークに接続できました。
Denoでは、明示的にフラグを指定しない限り、これらの操作は拒否されます。
// このコードを実行しても、権限がなければエラーになる
const data = await Deno.readTextFile("./secret.txt");
実行時に以下のようなプロンプトが表示されます。
┌ ⚠️ Deno requests read access to "./secret.txt".
├ Requested by `Deno.readTextFile()` API.
├ Learn more at: https://docs.deno.com/go/--allow-read
├ Run again with --allow-read to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >
これは「最小権限の原則」を体現しています。
人間で言えば、「必要な鍵だけ渡す」ようなものです。
全ての鍵を渡すのではなく、本当に必要な部屋の鍵だけを渡す。
4.2 権限フラグの一覧
| フラグ | 許可する内容 | 例 |
|---|---|---|
--allow-read |
ファイル読み取り | --allow-read=./data |
--allow-write |
ファイル書き込み | --allow-write=./logs |
--allow-net |
ネットワークアクセス | --allow-net=api.example.com |
--allow-env |
環境変数の読み取り | --allow-env=API_KEY |
--allow-run |
サブプロセスの実行 | --allow-run=git |
--allow-ffi |
外部ライブラリの呼び出し | FFI使用時 |
-A |
全ての権限を許可 | 開発時のみ推奨 |
スコープを限定することで、万が一悪意のあるコードが混入しても被害を最小化できます。
4.3 TypeScriptファーストクラスサポート
Node.jsでTypeScriptを使うには、以下の手順が必要でした。
-
typescriptパッケージをインストール -
tsconfig.jsonを設定 -
tscでコンパイル - コンパイル結果の
.jsファイルを実行
Denoでは、.tsファイルをそのまま実行できます。
# Node.jsの場合
npm install typescript
npx tsc main.ts
node main.js
# Denoの場合
deno run main.ts
設定ファイルも型定義ファイルも不要。
「書いたら動く」シンプルさがDenoの魅力です。
4.4 Web標準APIの採用
DenoはブラウザのWeb APIを可能な限り採用しています。
fetch、Request、Response、URL、WebSocketなどがそのまま使えます。
// ブラウザと同じfetch APIが使える
const response = await fetch("https://api.github.com/users/denoland");
const data = await response.json();
console.log(data.name);
これにより、フロントエンドとバックエンドで同じ知識が活かせます。
「ブラウザで動くコードは、Denoでもほぼそのまま動く」と考えてOKです。
4.5 組み込みツールチェーン
Denoは「バッテリー同梱」の思想を持っています。
Node.jsでは別途インストールが必要だった各種ツールが、最初から組み込まれています。
| ツール | コマンド | Node.jsの場合 |
|---|---|---|
| フォーマッター | deno fmt |
Prettier |
| リンター | deno lint |
ESLint |
| テストランナー | deno test |
Jest, Mocha等 |
| ドキュメント生成 | deno doc |
JSDoc等 |
| バンドラー | deno bundle |
webpack, esbuild等 |
| 実行ファイル化 | deno compile |
pkg等 |
| タスクランナー | deno task |
npm scripts |
基本概念が理解できたところで、実際に手を動かしてみましょう。
5. 実際に使ってみよう
5.1 環境構築
インストール方法
お使いのOSに合わせて、以下のコマンドを実行してください。
macOS / Linux:
curl -fsSL https://deno.land/install.sh | sh
Windows (PowerShell):
irm https://deno.land/install.ps1 | iex
Homebrew (macOS):
brew install deno
インストール後、以下のコマンドで確認します。
deno --version
実行結果
deno 2.6.1 (stable, release, aarch64-apple-darwin)
v8 13.4.114.9-rusty
typescript 5.7.3
5.2 設定ファイルの準備
以下の内容をdeno.jsonとして保存してください。
{
"compilerOptions": {
"strict": true,
"lib": ["deno.window"]
},
"imports": {
"@std/": "jsr:@std/"
},
"tasks": {
"dev": "deno run --watch --allow-net --allow-read main.ts",
"test": "deno test --allow-read",
"lint": "deno lint",
"fmt": "deno fmt"
},
"fmt": {
"useTabs": false,
"lineWidth": 80,
"indentWidth": 2,
"singleQuote": true,
"proseWrap": "preserve"
},
"lint": {
"rules": {
"tags": ["recommended"]
}
}
}
各設定の説明は以下の通りです。
| 項目 | 説明 |
|---|---|
compilerOptions |
TypeScriptコンパイラの設定 |
imports |
インポートマップ(エイリアス設定) |
tasks |
npm scriptsに相当するタスク定義 |
fmt |
フォーマッターの設定 |
lint |
リンターの設定 |
5.3 Hello World
main.tsファイルを作成します。
/**
* Deno入門: Hello Worldサンプル
* 実行方法: deno run main.ts
*/
function greet(name: string): string {
return `Hello, ${name}! Welcome to Deno.`;
}
const message = greet("World");
console.log(message);
// 現在のDenoバージョンを表示
console.log(`Deno version: ${Deno.version.deno}`);
console.log(`V8 version: ${Deno.version.v8}`);
console.log(`TypeScript version: ${Deno.version.typescript}`);
実行します。
deno run main.ts
実行結果
Hello, World! Welcome to Deno.
Deno version: 2.6.1
V8 version: 13.4.114.9-rusty
TypeScript version: 5.7.3
5.4 HTTPサーバーを作る
Denoの標準機能でHTTPサーバーを作ってみましょう。
server.tsを作成します。
/**
* シンプルなHTTPサーバー
* 実行方法: deno run --allow-net server.ts
*/
const PORT = 8000;
function handler(request: Request): Response {
const url = new URL(request.url);
// ルーティング
if (url.pathname === "/") {
return new Response("Hello from Deno Server!", {
status: 200,
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
if (url.pathname === "/api/time") {
const now = new Date().toISOString();
return Response.json({ time: now });
}
// 404 Not Found
return new Response("Not Found", { status: 404 });
}
console.log(`Server running at http://localhost:${PORT}/`);
Deno.serve({ port: PORT }, handler);
実行します。
deno run --allow-net server.ts
実行結果
Server running at http://localhost:8000/
Listening on http://0.0.0.0:8000/
ブラウザでhttp://localhost:8000/にアクセスすると、メッセージが表示されます。
http://localhost:8000/api/timeにアクセスすると、JSON形式で現在時刻が返ります。
5.5 外部パッケージを使う
Denoでは、JSR(JavaScript Registry)やnpmからパッケージをインポートできます。
# JSRからパッケージを追加
deno add jsr:@std/datetime
# npmパッケージを追加
deno add npm:chalk@5
deno.jsonに依存関係が追加されます。
{
"imports": {
"@std/datetime": "jsr:@std/datetime@^0.225.2",
"chalk": "npm:chalk@5"
}
}
使用例を作成します。
/**
* 外部パッケージの使用例
* 実行方法: deno run --allow-env packages_example.ts
*/
import { format } from "@std/datetime";
import chalk from "chalk";
const now = new Date();
const formatted = format(now, "yyyy-MM-dd HH:mm:ss");
console.log(chalk.green("現在の日時:"));
console.log(chalk.blue(formatted));
console.log(chalk.yellow("Denoで外部パッケージが使えました!"));
5.6 テストを書く
Denoは組み込みのテストランナーを持っています。
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 function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}
math_test.tsを作成します。
/**
* 数学ユーティリティのテスト
* 実行方法: deno test math_test.ts
*/
import { assertEquals, assertThrows } from "jsr:@std/assert";
import { add, multiply, divide } from "./math.ts";
Deno.test("add: 正の数の加算", () => {
assertEquals(add(2, 3), 5);
});
Deno.test("add: 負の数の加算", () => {
assertEquals(add(-2, -3), -5);
});
Deno.test("multiply: 掛け算", () => {
assertEquals(multiply(4, 5), 20);
});
Deno.test("divide: 正常な割り算", () => {
assertEquals(divide(10, 2), 5);
});
Deno.test("divide: ゼロ除算でエラー", () => {
assertThrows(
() => divide(10, 0),
Error,
"Division by zero is not allowed"
);
});
テストを実行します。
deno test
実行結果
running 5 tests from ./math_test.ts
add: 正の数の加算 ... ok (1ms)
add: 負の数の加算 ... ok (0ms)
multiply: 掛け算 ... ok (0ms)
divide: 正常な割り算 ... ok (0ms)
divide: ゼロ除算でエラー ... ok (0ms)
ok | 5 passed | 0 failed (15ms)
5.7 よくあるエラーと対処法
| エラー | 原因 | 対処法 |
|---|---|---|
PermissionDenied: read access |
ファイル読み取り権限がない |
--allow-readフラグを追加 |
PermissionDenied: net access |
ネットワーク権限がない |
--allow-netフラグを追加 |
Module not found |
インポートパスが間違っている | 拡張子(.ts)を含めて指定 |
Cannot find name 'require' |
CommonJS構文を使っている | ESM形式のimportに変更 |
error: Specifier not found |
パッケージが見つからない |
deno addでパッケージを追加 |
私が最初にハマったのは、requireを使ってエラーになったことです。
Node.js脳でconst fs = require('fs')と書いてしまい、動かない。
「ESMで書く」というルールを忘れていました。
基本的な使い方をマスターしたので、次は応用例を見ていきましょう。
6. 応用例・発展
6.1 Node.jsプロジェクトをDenoで動かす
Deno 2.0からは、既存のNode.jsプロジェクトをそのまま動かせます。
# package.jsonがあるディレクトリで
deno install
deno task dev # npm run devの代わり
package.jsonとnode_modulesをDenoが理解します。
段階的な移行が可能になりました。
6.2 実行ファイルを作成する
JavaScriptをネイティブ実行ファイルにコンパイルできます。
# 現在のOS向け
deno compile --allow-net server.ts
# Windows向けにクロスコンパイル
deno compile --allow-net --target x86_64-pc-windows-msvc server.ts
# macOS向け
deno compile --allow-net --target x86_64-apple-darwin server.ts
Denoがインストールされていない環境でも、生成された実行ファイルを配布できます。
6.3 さらなる学習リソース
7. まとめ
この記事では、Denoについて以下を解説しました。
- Denoの誕生背景: Node.js作者の「後悔」から生まれた次世代ランタイム
- セキュリティモデル: デフォルトで何もできない、権限明示が必要な設計
- TypeScriptサポート: 設定不要でTypeScriptを直接実行
- 組み込みツール: フォーマッター、リンター、テストランナーが標準装備
- Node.js互換性: Deno 2.0でpackage.jsonとnode_modulesに対応
私の所感
正直に言うと、Denoが「Node.jsを置き換える」かどうかはわかりません。
Node.jsのエコシステムはあまりにも巨大で、簡単には移行できない現実があります。
しかし、新規プロジェクトでDenoを選択する価値は十分にあると感じています。
特に以下の点が魅力的です。
- ゼロコンフィグ: 設定ファイルの山と格闘しなくていい
- セキュリティ: 「何も許可しない」からスタートする安心感
- TypeScript: 「書いたら動く」シンプルさ
Ryan Dahl氏が「後悔」を語れたのは、自分の過去を直視できる強さがあるからだと思います。
その強さから生まれたDenoには、未来のJavaScript開発のヒントが詰まっています。
ぜひ一度、deno runを試してみてください。
Node.jsとは違う、軽やかな開発体験が待っています。