Use Cases (更新: 2026/6/7)

Deno入門:Nodeとの違いと--allow権限を、動くAPIで理解する

Deno入門。Nodeとの違い(標準でTS・URLインポート・権限モデル)、deno.json、--allow権限、npm互換、Deno Deployまで。コピペで動くAPIとテストで一気につかむ。

Deno入門:Nodeとの違いと--allow権限を、動くAPIで理解する

「環境変数、勝手に読まれてたんだ」

ある日、依存ライブラリの一つがこっそりprocess.envを全部かき集めて外に投げていた、という話を読んで背筋が寒くなりました。Nodeでは、npm installしたコードはあなたと同じ権限で動きます。ファイルもネットワークも環境変数も、止める仕組みは最初から入っていない。気づいたときには、もう外に出たあとです。

Denoは、そこを真逆にしたランタイムです。ランタイムというのは「コードを動かす土台」のこと。Denoでは、コードは初期状態でファイルもネットワークも一切触れません。触らせたいときだけ、こちらが--allow-netのように明示的に許可する。許可していない操作は、その場で止まります。

僕はこの「最初は全部閉じている」感覚が好きで、検証用の小さいAPIや自動化スクリプトはDenoで書くことが増えました。この記事では、Nodeとの違いを押さえつつ、コピペで動くDenoのAPIとテストまで一気に作ります。

この記事の要点

  • Denoは初期状態で全権限オフ--allow-read--allow-netで必要な分だけ開ける。これがNodeとの一番大きな違い。
  • TypeScriptがそのまま動く。ビルド設定(tsconfig、ts-node、Babel)が要らない。
  • deno.json一つにタスク・依存・整形・lintをまとめられる。設定ファイルの数が激減する。
  • npmも使えるnpm:honoのように書けばnpm installなしで読み込める。Nodeからの移行や併用がしやすい。
  • 標準で整形・lint・テストが同梱。deno fmt deno lint deno testだけで品質の底上げができる。
  • 小さいAPI・CLI・自動化・エッジ(Deno Deploy)に強い。逆に巨大なNode資産があるなら無理に乗り換えない。

Denoは何が違うのか:Nodeとの比較

「Node.jsの作者が、Nodeの反省点を直して作り直したのがDeno」とよく言われます。実際そのとおりで、違いはだいたい次の4つに集約されます。

観点Node.jsDeno
権限入れたコードはあなたと同権限。制限なし初期は全オフ。--allow-*で個別に許可
TypeScripttsconfig + ビルドやts-nodeが必要そのままdeno run app.tsで動く
依存の取り込みnpm installnode_modulesnpm:やURL指定で都度キャッシュ。npm install不要
周辺ツール整形・lint・テストは別途導入整形・lint・テストが標準同梱

順番に、もう少しだけ噛み砕きます。

権限モデル。これがDenoの背骨です。コードに「どこまで触らせるか」を、実行時のフラグで決めます。フラグは目的ごとに分かれていて、--allow-read(ファイル読み取り)、--allow-write(書き込み)、--allow-net(ネットワーク)、--allow-env(環境変数)、--allow-run(外部コマンド実行)、--allow-ffi(ネイティブライブラリ)、--allow-sys(OS情報)があります。逆に「これだけは絶対ダメ」を指定する--deny-netのような拒否フラグもあり、こちらが許可より優先されます。詳しくはDeno公式のSecurity and permissionsが一次情報です。

TypeScriptが素で動く。Nodeでtsを動かすには、tsconfigを書いてビルドするか、ts-nodeのような道具を挟む必要がありました。Denoは.tsファイルをそのまま実行できます。「型を書きたいだけなのに設定で30分溶かす」が消えるのは、地味に効きます。

依存の取り込み方。Nodeはnpm installしてnode_modulesを作るのが前提でした。Denoはnpm:プレフィックスやURLで都度取り込み、グローバルにキャッシュします。後で詳しく触れますが、npmパッケージもそのまま使えるので「Denoに移ったらライブラリ資産を捨てる」わけではありません。

—allow権限:Denoのいちばん大事な所

ここを理解すると、Denoの設計思想が一気に腑に落ちます。

たとえばネットワークを使うサーバーを「全許可」で動かすなら、こう書けます。

deno run -A server.ts

-A--allow-allの短縮で、全権限を開く=サンドボックスを丸ごと無効化します。これだと結局Nodeと同じ無防備さになるので、Denoを選ぶ意味が半減します。公式も「セキュリティの砂場を無効化するので慎重に」と明記しています。

正しくは、必要な範囲だけ開けます。ホストとポート、読み書きするディレクトリまで絞れるのがポイントです。

deno run \
  --allow-net=127.0.0.1:8000 \
  --allow-read=./data \
  --allow-write=./data \
  server.ts

これで、たとえ依存ライブラリがこっそり~/.sshを読もうとしても、./dataの外なのでDenoが止めます。外部に勝手な通信をしようとしても、127.0.0.1:8000以外は通りません。「事故っても被害が./dataの中だけ」に閉じ込められるわけです。

許可を渡し忘れると、こんなふうに実行時に止まります。

error: Uncaught (in promise) NotCapable: Requires net access to "127.0.0.1:8000",
run again with the --allow-net flag

このエラーは敵ではなく、味方です。「いま、このコードはネットワークを使おうとしたよ」と教えてくれている。Nodeなら黙って通っていた操作が、可視化されるんです。

deno.json:設定ファイルを一つにまとめる

Nodeだと、package.json・tsconfig.json・.prettierrc・.eslintrc…と設定ファイルが散らかりがちです。Denoはdeno.json(コメントを書きたいならdeno.jsonc)一つに、タスク・依存・整形・lintをまとめられます。しかも置いておけば自動で見つけてくれます。

毎回長い--allow-*を打つのは面倒なので、よく使うコマンドはtasksに名前を付けて固定します。checkは整形チェック・lint・テストを一気に走らせる「品質の関所」です。

{
  "tasks": {
    "dev": "deno run --watch --allow-net=127.0.0.1:8000 --allow-read=./data --allow-write=./data server.ts",
    "start": "deno run --allow-net=127.0.0.1:8000 --allow-read=./data --allow-write=./data server.ts",
    "fmt": "deno fmt",
    "lint": "deno lint",
    "test": "deno test",
    "check": "deno fmt --check && deno lint && deno test"
  },
  "fmt": {
    "lineWidth": 100,
    "semiColons": true
  },
  "lint": {
    "rules": {
      "tags": ["recommended"]
    }
  },
  "imports": {
    "@std/assert": "jsr:@std/assert"
  }
}

importsは「この別名を、この実体に対応させる」という対応表(インポートマップ)です。上の例では@std/assertという短い名前で標準ライブラリを呼べるようにしています。deno fmt deno lint deno testはDenoに最初から入っているので、個人開発ならこの3つだけでも十分です。チームならcheckをCIに入れて、「整形してない」「lint違反」「テスト失敗」を同じ入口で止めるのが楽です。設定できる項目はDeno公式のConfigurationにまとまっています。

ちなみに標準ライブラリは今、JSRというレジストリの@stdスコープにあります。jsr:@std/assertのように指定するのが現在の書き方で、@std/fs(ファイル操作)や@std/http(HTTP補助)など機能ごとに分かれています。

Deno.serveでコピーして動くAPIを作る

説明より動かすほうが早いです。Web標準のRequestを受け取ってResponseを返すだけの小さなJSON APIを作ります。Express(Nodeの定番フレームワーク)は使いません。Deno.serveはDenoに組み込みのサーバー起動関数で、公式リファレンスはDeno.serveです。

まず本体。データの保存先を差し替えられるよう、ItemStoreという形(インターフェース)に依存させておきます。あとでテストが楽になる仕込みです。

// app.ts
export type Item = {
  id: string;
  title: string;
  done: boolean;
};

export interface ItemStore {
  list(): Promise<Item[]>;
  save(items: Item[]): Promise<void>;
}

// 実体はJSONファイル。読めなければ空配列を返す
export class FileItemStore implements ItemStore {
  constructor(private readonly path = "./data/items.json") {}

  async list(): Promise<Item[]> {
    try {
      const text = await Deno.readTextFile(this.path);
      return JSON.parse(text) as Item[];
    } catch (error) {
      if (error instanceof Deno.errors.NotFound) {
        return [];
      }
      throw error;
    }
  }

  async save(items: Item[]): Promise<void> {
    await Deno.writeTextFile(this.path, JSON.stringify(items, null, 2));
  }
}

// リクエストを振り分けるハンドラ。storeは外から渡す
export function createHandler(store: ItemStore): (request: Request) => Promise<Response> {
  return async (request: Request): Promise<Response> => {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return Response.json({ ok: true });
    }

    if (url.pathname === "/api/items" && request.method === "GET") {
      return Response.json(await store.list());
    }

    if (url.pathname === "/api/items" && request.method === "POST") {
      const body = await request.json().catch(() => null) as { title?: unknown } | null;

      // タイトルが空ならここで弾く
      if (!body || typeof body.title !== "string" || body.title.trim() === "") {
        return Response.json({ error: "title is required" }, { status: 400 });
      }

      const items = await store.list();
      const item: Item = {
        id: crypto.randomUUID(),
        title: body.title.trim(),
        done: false,
      };

      await store.save([...items, item]);
      return Response.json(item, { status: 201 });
    }

    return new Response("Not Found", { status: 404 });
  };
}
// server.ts
import { createHandler, FileItemStore } from "./app.ts";

Deno.serve(
  { hostname: "127.0.0.1", port: 8000 },
  createHandler(new FileItemStore()),
);

実行前に保存先ファイルを用意して、起動します。

mkdir -p data
printf "[]\n" > data/items.json
deno task dev

別のターミナルから叩いて確認します。

curl http://127.0.0.1:8000/health
curl -X POST http://127.0.0.1:8000/api/items \
  -H "content-type: application/json" \
  -d '{"title":"Denoの記事ネタ"}'
curl http://127.0.0.1:8000/api/items

このAPIは、起動にネットワーク権限、JSON読み取りにread権限、追記にwrite権限が要ります。deno.jsondevタスクで対象を127.0.0.1:8000./dataに絞っているので、もし将来うっかりホーム全体を読むコードが混ざっても、Denoが止めてくれます。crypto.randomUUID()URLRequest/Responseがそのまま使えるのも、DenoがWeb標準APIを基準にしているからです。

Deno.testで権限なしの単体テストを書く

さっきItemStoreを外から渡せるようにした理由が、ここで効きます。テストではファイルではなくメモリ上の実装を差し込みます。すると、ファイルもネットワークも触らないので、**権限フラグなしのdeno test**だけでテストが回ります。

// app_test.ts
import { assertEquals } from "@std/assert";
import { createHandler, type Item, type ItemStore } from "./app.ts";

// テスト専用。ディスクではなく配列に貯める
class MemoryStore implements ItemStore {
  private items: Item[] = [];

  async list(): Promise<Item[]> {
    return [...this.items];
  }

  async save(items: Item[]): Promise<void> {
    this.items = [...items];
  }
}

Deno.test("GET /health は ok を返す", async () => {
  const handler = createHandler(new MemoryStore());
  const response = await handler(new Request("http://localhost/health"));

  assertEquals(response.status, 200);
  assertEquals(await response.json(), { ok: true });
});

Deno.test("POST /api/items は item を作る", async () => {
  const handler = createHandler(new MemoryStore());
  const response = await handler(
    new Request("http://localhost/api/items", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ title: "記事を書く" }),
    }),
  );

  const created = await response.json() as Item;

  assertEquals(response.status, 201);
  assertEquals(created.title, "記事を書く");
  assertEquals(created.done, false);
});

Deno.test("POST /api/items は空タイトルを拒否する", async () => {
  const handler = createHandler(new MemoryStore());
  const response = await handler(
    new Request("http://localhost/api/items", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ title: "" }),
    }),
  );

  assertEquals(response.status, 400);
});

deno testは実行時権限と同じ考え方で動きます。テストに余計な権限を与えない設計にしておくと、後からコードに危ないI/Oが紛れ込んだとき、テストが権限エラーで気づかせてくれます。詳細は公式のdeno testを見てください。

npm互換:Nodeの資産はそのまま使える

「Denoに行くとライブラリが使えないのでは」という不安、僕も最初ありました。結論、ほぼ杞憂です。npmパッケージはnpm:プレフィックスで取り込めます。npm installnode_modulesも要りません。

// npmのHonoをそのまま使う例
import { Hono } from "npm:hono";

const app = new Hono();
app.get("/", (c) => c.text("Hello from npm:hono on Deno"));

Deno.serve(app.fetch);

Node組み込みモジュール(pathやosなど)はnode:を付ければ呼べます。

import path from "node:path";
import { Buffer } from "node:buffer";

さらにDenoはpackage.jsonも読めるので、既存Nodeプロジェクトにdenoコマンドを足して少しずつ移す、という併用もやりやすい。「全部いっぺんに乗り換える」必要はなく、スクリプト一本からDenoに寄せていけます。詳しいルールは公式のNode and npm supportにあります。

いつDenoを選ぶか(と、選ばない方がいい場面)

道具に善悪はなく、向き不向きだけがあります。僕がDenoを選ぶのは、だいたいこの4パターンです。

  1. Expressを立てるほどでもない小さなAPI。管理画面の試作、CSV取り込みの確認、Webhook受信あたりは、Deno.serveとJSONファイルで足ります。「本番DBではなくJSONでいい」「権限は./data配下だけ」と方針を決めておくと、検証用として扱いやすい。
  2. リポジトリ内の自動化スクリプト。記事一覧の整形、設定ファイルの検査、APIレスポンスの保存などは、tsが素で動いて標準ツールが揃うDenoが軽快です。package.jsonを増やさずdeno task checkだけで品質確認をそろえられます。
  3. 学習・チーム導入の教材。権限エラーがはっきり出るので、「なぜこのスクリプトにネットワーク権限が要るのか」を説明しやすい。セキュリティ意識を体で覚えてもらうのに向いています。
  4. エッジ(Deno Deploy)へ寄せたいHTTP処理。Deno Deployは公式のホスティング基盤で、最初からWeb標準のRequest/Responseで書いておけば、フレームワーク依存が薄く移植の判断が楽になります。デプロイ先固有の制約はDeno Deployで確認してください。

逆に、巨大なNode資産があってチームもNode前提なら、無理に乗り換える必要はありません。動いているものを止めてまで移す価値があるかは、別問題です。新規の小さい部品からDenoを試す、くらいがちょうどいい温度感だと思います。

初心者がはまりやすい落とし穴

僕が実際にやらかした順に挙げます。

最初の落とし穴は、-Aを「とりあえず」で残すこと。動かしたい一心で全権限を開くと、Denoの安全装置が丸ごと無効になります。ローカル検証でも--allow-read=./dataのように対象を絞る癖をつけたほうがいい。

次は、Nodeの癖でExpress・Jest・Prettier・ESLintを一気に足すこと。もちろん必要なら使えますが、入門段階ではDeno.servedeno fmtdeno lintDeno.test()を先に触ったほうが、Denoの形がつかめます。

三つ目は、書き込み先ディレクトリの作り忘れ。上の例で先にdata/items.jsonを作っているのはこのためです。「無ければディレクトリも作る」まで実装するなら、Deno.mkdirとwrite権限の扱いを明示します。

四つ目は、テストに本物のファイルやネットワークを使いすぎること。単体テストはメモリ実装で速く保ち、ファイルI/Oの確認は別の結合テストに分けると、権限も失敗原因も読みやすくなります。

最後は、古いサンプルをそのまま貼ること。Denoは進化が速く、標準ライブラリの置き場(今はJSR)や設定の書き方が変わってきました。実装前に公式ページで現在の書き方を確認するのが、結局いちばん速い回り道です。

よくある質問

Q. DenoはNodeの代わりになりますか? A. 用途によります。小さいAPI・CLI・自動化・エッジなら十分代わりになります。ただしNode専用の重い周辺ツールに依存した大規模プロジェクトは、移行コストが見合わないこともあります。新規の小さい部品から試すのがおすすめです。

Q. --allowを毎回打つのが面倒です。 A. deno.jsontasksに権限込みのコマンドを登録すれば、deno task devだけで済みます。この記事のdeno.jsonがそのまま使えます。-Aで全部開くのは安全装置を切ることなので避けてください。

Q. npmのライブラリは本当に使えますか? A. 使えます。import x from "npm:パッケージ名"と書くだけで、npm installなしに読み込めます。Node組み込みはnode:pathのようにnode:を付けます。

Q. TypeScriptの設定(tsconfig)は要りますか? A. 基本は不要です。.tsをそのままdeno runできます。コンパイラ設定を変えたいときだけdeno.jsoncompilerOptionsで調整します。

Q. 作ったAPIはどこにデプロイできますか? A. 公式のDeno Deployがエッジ実行に向いています。Web標準のRequest/Responseで書いておけば移植が楽です。デプロイ先固有の制約は公式ドキュメントで確認してください。

関連記事と次の一歩

Denoの安全な小さいAPIをClaude Codeに作らせる流れは、Claude Code入門ガイドで土台を押さえ、本格的なAPI設計はClaude Code API開発、テストの組み立てはテスト戦略ガイドが参考になります。Claude Codeに頼むときは「Deno.serveを使う」「Expressは使わない」「権限は./data127.0.0.1:8000だけ」「-A禁止」のように、使うAPI・使わないライブラリ・権限・検証コマンドを同時に渡すと出力が安定します。

繰り返し使う依頼文やレビュー用プロンプトを手元に固めたいなら、教材・テンプレートが近道です。

実際に試した結果

冒頭の「環境変数が勝手に読まれていた」話以来、僕はランタイムを「速いか」より「事故が外に出るか」で見るようになりました。Denoで--allow-read=./dataだけ開けて検証スクリプトを回すと、依存が変な所を触った瞬間にNotCapableで止まる。あの「ちゃんと止まった」という安心感は、Nodeでは得られなかったものです。

正直、巨大なNodeプロジェクトを今すぐ全部Denoにする気はありません。でも、新しく書く小さいAPIや自動化は、ほぼDenoに寄せました。速さの差より、「転んでも./dataの中で済む」という設計のほうが、長い目で見て効いてくる。まずはこの記事のdeno.jsonserver.tsをコピーして、--allow-netを一度わざと外して怒られてみてください。Denoの一番おいしい所が、たぶん一発で伝わります。

#Deno #Node.js #TypeScript #ランタイム #権限管理
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。