Deno Freshは、ゼロビルドステップ・Islands Architecture・TypeScriptネイティブという特徴を持つ次世代のフルスタックWebフレームワークです。Node.js + Next.jsの組み合わせに疲れたエンジニアから注目を集めており、2026年にはSES案件でもDeno採用プロジェクトが増加傾向にあります。
本記事では、Gemini CLIを活用してDeno Freshのフルスタック開発を効率的に行う方法を解説します。プロジェクト構築からIslands Architectureの実装、データベース連携、Deno Deployへのデプロイまで、AIと協働して高品質なWebアプリケーションを構築するテクニックをお伝えします。

Deno Freshとは何か:Next.jsとの違いを理解する
Deno Freshの主な特徴
Deno Freshは、Deno公式のフルスタックWebフレームワークで、以下の特徴があります。
- ゼロビルドステップ: バンドラー不要、TypeScriptをそのまま実行
- Islands Architecture: ページ全体ではなく、インタラクティブな「島」だけをクライアントサイドでハイドレーション
- サーバーサイドレンダリング: デフォルトでSSR、高速な初期表示
- TypeScriptネイティブ: 設定不要でTypeScriptを利用可能
- Web標準API: Fetch API、Request/Response、Web Streams等のWeb標準に準拠
- Deno Deploy統合: エッジデプロイが数クリックで完了
Fresh vs Next.js:比較表
| 項目 | Deno Fresh | Next.js |
|---|---|---|
| ランタイム | Deno | Node.js |
| 言語 | TypeScript(ネイティブ) | TypeScript(tsc必要) |
| ビルドステップ | 不要 | webpack/Turbopack |
| レンダリング | SSR + Islands | SSR/SSG/ISR/RSC |
| バンドルサイズ | 極小(Islandsのみ) | フレームワーク全体 |
| 設定ファイル | 最小限 | next.config.js等多数 |
| パッケージ管理 | URL import / deno.json | npm / package.json |
| デプロイ | Deno Deploy | Vercel等 |
| エコシステム | 成長中 | 非常に充実 |
| 学習コスト | 低〜中 | 中〜高 |
なぜ今Deno Freshなのか
2026年の開発トレンドとして、以下の理由でDeno Freshが注目されています。
- パフォーマンス重視: Islands Architectureにより、クライアントに送信するJavaScriptを最小化
- 開発体験: ビルドステップ不要で、保存即反映のホットリロード
- セキュリティ: Denoの権限モデルにより、デフォルトでセキュア
- エッジ対応: Deno Deployで世界35+のエッジロケーションにデプロイ
Gemini CLIでDeno Freshプロジェクトを構築する
プロジェクト初期化
# Gemini CLIにプロジェクト構築を依頼
gemini "Deno FreshでSaaS向けの管理ダッシュボードを作成したい。
以下の機能を含むプロジェクト構造を設計してください:
- ユーザー認証(OAuth2.0 / GitHub)
- ダッシュボードページ(チャート表示)
- ユーザー管理CRUD
- API Routes(REST API)
- PostgreSQL接続
- Tailwind CSS
プロジェクト構造とdeno.jsonの設定も含めてください"
Gemini CLIが生成するプロジェクト構造:
fresh-dashboard/
├── components/ # 共通コンポーネント(非インタラクティブ)
│ ├── Header.tsx
│ ├── Footer.tsx
│ ├── Sidebar.tsx
│ └── Card.tsx
├── islands/ # Islandsコンポーネント(インタラクティブ)
│ ├── Chart.tsx
│ ├── UserTable.tsx
│ ├── SearchBar.tsx
│ └── ThemeToggle.tsx
├── routes/ # ルーティング
│ ├── _app.tsx
│ ├── _layout.tsx
│ ├── index.tsx
│ ├── dashboard.tsx
│ ├── users/
│ │ ├── index.tsx
│ │ └── [id].tsx
│ └── api/
│ ├── users/
│ │ ├── index.ts
│ │ └── [id].ts
│ └── auth/
│ ├── login.ts
│ └── callback.ts
├── utils/
│ ├── db.ts
│ ├── auth.ts
│ └── api.ts
├── static/
│ └── styles.css
├── deno.json
├── fresh.config.ts
├── dev.ts
└── main.ts
deno.jsonの設定
{
"lock": false,
"tasks": {
"check": "deno fmt --check && deno lint && deno check **/*.ts **/*.tsx",
"start": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"db:migrate": "deno run -A utils/migrate.ts",
"test": "deno test -A --parallel"
},
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.7.3/",
"preact": "https://esm.sh/preact@10.24.3",
"preact/": "https://esm.sh/preact@10.24.3/",
"@preact/signals": "https://esm.sh/@preact/signals@1.3.1",
"$std/": "https://deno.land/std@0.224.0/",
"postgres": "https://deno.land/x/postgres@v0.19.3/mod.ts",
"chart.js": "https://esm.sh/chart.js@4.4.6",
"tailwindcss": "npm:tailwindcss@3.4.17"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
Islands Architectureの実践:インタラクティブコンポーネントの設計
Islands Architectureの仕組み
Islands Architectureでは、ページの大部分は静的なHTMLとしてサーバーでレンダリングされ、インタラクティブな要素だけがクライアントサイドのJavaScriptとしてハイドレーションされます。
gemini "Deno Freshのislands/配下にダッシュボード用のチャートコンポーネントを作成してください。
Chart.jsを使い、以下の要件を満たすこと:
- 売上推移の折れ線グラフ
- ユーザー数の棒グラフ
- レスポンシブ対応
- ダークモード対応
- データはAPIから非同期取得"
チャートIslandコンポーネント
// islands/Chart.tsx
import { useEffect, useRef, useState } from "preact/hooks";
import { IS_BROWSER } from "$fresh/runtime.ts";
interface ChartData {
labels: string[];
datasets: {
label: string;
data: number[];
borderColor?: string;
backgroundColor?: string;
}[];
}
interface ChartProps {
type: "line" | "bar" | "doughnut";
apiEndpoint: string;
title: string;
height?: number;
}
export default function Chart({ type, apiEndpoint, title, height = 300 }: ChartProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!IS_BROWSER) return;
let chart: any = null;
const loadChart = async () => {
try {
// Chart.jsを動的インポート
const { Chart, registerables } = await import("chart.js");
Chart.register(...registerables);
// APIからデータ取得
const response = await fetch(apiEndpoint);
if (!response.ok) throw new Error("データの取得に失敗しました");
const data: ChartData = await response.json();
if (canvasRef.current) {
chart = new Chart(canvasRef.current, {
type,
data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: { display: true, text: title },
legend: { position: "bottom" },
},
scales: type !== "doughnut" ? {
y: { beginAtZero: true },
} : undefined,
},
});
}
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : "エラーが発生しました");
setLoading(false);
}
};
loadChart();
return () => {
if (chart) chart.destroy();
};
}, [apiEndpoint, type, title]);
if (!IS_BROWSER) {
return (
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-lg font-semibold mb-4">{title}</h3>
<div style={{ height: `${height}px` }} class="flex items-center justify-center">
<span class="text-gray-400">チャートを読み込み中...</span>
</div>
</div>
);
}
return (
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-lg font-semibold mb-4 dark:text-white">{title}</h3>
{loading && (
<div style={{ height: `${height}px` }} class="flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
</div>
)}
{error && (
<div class="text-red-500 text-center py-4">{error}</div>
)}
<div style={{ height: `${height}px`, display: loading ? "none" : "block" }}>
<canvas ref={canvasRef} />
</div>
</div>
);
}
検索バーIslandコンポーネント
// islands/SearchBar.tsx
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
interface SearchBarProps {
placeholder?: string;
onSearch: string; // API endpoint
}
export default function SearchBar({ placeholder = "検索...", onSearch }: SearchBarProps) {
const query = useSignal("");
const results = useSignal<any[]>([]);
const isOpen = useSignal(false);
const loading = useSignal(false);
useEffect(() => {
const timer = setTimeout(async () => {
if (query.value.length < 2) {
results.value = [];
isOpen.value = false;
return;
}
loading.value = true;
try {
const res = await fetch(`${onSearch}?q=${encodeURIComponent(query.value)}`);
const data = await res.json();
results.value = data.items || [];
isOpen.value = true;
} catch {
results.value = [];
} finally {
loading.value = false;
}
}, 300); // デバウンス
return () => clearTimeout(timer);
}, [query.value]);
return (
<div class="relative w-full max-w-md">
<input
type="text"
value={query.value}
onInput={(e) => query.value = (e.target as HTMLInputElement).value}
placeholder={placeholder}
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500
dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
{loading.value && (
<div class="absolute right-3 top-3">
<div class="animate-spin h-4 w-4 border-2 border-blue-500 rounded-full border-t-transparent" />
</div>
)}
{isOpen.value && results.value.length > 0 && (
<div class="absolute z-10 w-full mt-1 bg-white dark:bg-gray-800
border rounded-lg shadow-lg max-h-60 overflow-y-auto">
{results.value.map((item: any) => (
<a
href={item.url}
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700
dark:text-white border-b last:border-b-0"
>
<div class="font-medium">{item.name}</div>
<div class="text-sm text-gray-500">{item.description}</div>
</a>
))}
</div>
)}
</div>
);
}
API Routesの実装:Gemini CLIでRESTful APIを構築する
ユーザーCRUD API
gemini "Deno FreshのAPI RoutesでユーザーCRUD APIを作成してください。
要件:
- PostgreSQL接続(deno-postgres使用)
- 入力バリデーション
- 適切なHTTPステータスコード
- ページネーション対応
- エラーハンドリング
- CORS対応"
// routes/api/users/index.ts
import { Handlers } from "$fresh/server.ts";
import { pool } from "../../../utils/db.ts";
interface CreateUserInput {
email: string;
name: string;
role?: "admin" | "member" | "viewer";
}
function validateInput(input: unknown): CreateUserInput {
if (!input || typeof input !== "object") {
throw new ValidationError("リクエストボディが不正です");
}
const { email, name, role } = input as Record<string, unknown>;
if (!email || typeof email !== "string" || !email.includes("@")) {
throw new ValidationError("有効なメールアドレスを入力してください");
}
if (!name || typeof name !== "string" || name.length < 2) {
throw new ValidationError("名前は2文字以上で入力してください");
}
const validRoles = ["admin", "member", "viewer"];
const userRole = (role as string) || "member";
if (!validRoles.includes(userRole)) {
throw new ValidationError("無効なロールです");
}
return { email: email as string, name: name as string, role: userRole as CreateUserInput["role"] };
}
export const handler: Handlers = {
// GET /api/users - ユーザーリスト取得
async GET(req) {
const url = new URL(req.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = Math.min(parseInt(url.searchParams.get("limit") || "20"), 100);
const offset = (page - 1) * limit;
const search = url.searchParams.get("q") || "";
const client = await pool.connect();
try {
let query = "SELECT * FROM users WHERE status != 'DELETED'";
const params: unknown[] = [];
if (search) {
params.push(`%${search}%`);
query += ` AND (name ILIKE $${params.length} OR email ILIKE $${params.length})`;
}
// 件数取得
const countResult = await client.queryObject<{ count: number }>(
`SELECT COUNT(*) as count FROM (${query}) t`, params
);
const total = Number(countResult.rows[0].count);
// データ取得
params.push(limit, offset);
query += ` ORDER BY created_at DESC LIMIT $${params.length - 1} OFFSET $${params.length}`;
const result = await client.queryObject(query, params);
return new Response(JSON.stringify({
items: result.rows,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
}), {
headers: { "Content-Type": "application/json" },
});
} finally {
client.release();
}
},
// POST /api/users - ユーザー作成
async POST(req) {
try {
const body = await req.json();
const input = validateInput(body);
const client = await pool.connect();
try {
// 重複チェック
const existing = await client.queryObject(
"SELECT id FROM users WHERE email = $1", [input.email]
);
if (existing.rows.length > 0) {
return new Response(JSON.stringify({
error: "DUPLICATE_EMAIL",
message: "このメールアドレスは既に登録されています",
}), { status: 409, headers: { "Content-Type": "application/json" } });
}
const result = await client.queryObject(
`INSERT INTO users (email, name, role) VALUES ($1, $2, $3) RETURNING *`,
[input.email, input.name, input.role]
);
return new Response(JSON.stringify(result.rows[0]), {
status: 201,
headers: { "Content-Type": "application/json" },
});
} finally {
client.release();
}
} catch (err) {
if (err instanceof ValidationError) {
return new Response(JSON.stringify({
error: "VALIDATION_ERROR",
message: err.message,
}), { status: 400, headers: { "Content-Type": "application/json" } });
}
throw err;
}
},
};
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
データベース接続設定
// utils/db.ts
import { Pool } from "postgres";
const POOL_CONNECTIONS = 20;
const pool = new Pool({
hostname: Deno.env.get("DB_HOST") || "localhost",
port: parseInt(Deno.env.get("DB_PORT") || "5432"),
database: Deno.env.get("DB_NAME") || "freshdb",
user: Deno.env.get("DB_USER") || "postgres",
password: Deno.env.get("DB_PASSWORD") || "postgres",
}, POOL_CONNECTIONS);
export { pool };
認証の実装:OAuth2.0をGemini CLIで構築する
gemini "Deno FreshでGitHub OAuth2.0認証を実装してください。
要件:
- ログイン・ログアウト
- セッション管理(Cookie)
- ミドルウェアでの認証チェック
- 保護されたルートへのアクセス制御"
認証ミドルウェア
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
import { getCookies } from "$std/http/cookie.ts";
interface State {
user: { id: string; name: string; email: string } | null;
}
const PUBLIC_ROUTES = ["/", "/api/auth/login", "/api/auth/callback"];
export async function handler(req: Request, ctx: FreshContext<State>) {
const url = new URL(req.url);
// 公開ルートはスキップ
if (PUBLIC_ROUTES.some(route => url.pathname === route)) {
ctx.state.user = null;
return ctx.next();
}
// セッショントークンを検証
const cookies = getCookies(req.headers);
const sessionToken = cookies["session"];
if (!sessionToken) {
if (url.pathname.startsWith("/api/")) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
return new Response(null, {
status: 302,
headers: { Location: "/api/auth/login" },
});
}
// セッションからユーザー情報取得
const user = await getSessionUser(sessionToken);
if (!user) {
return new Response(null, {
status: 302,
headers: { Location: "/api/auth/login" },
});
}
ctx.state.user = user;
return ctx.next();
}
テスト戦略:Gemini CLIでDeno Freshのテストを自動化する
APIルートのテスト
gemini "Deno FreshのAPI Routesテストを作成してください。
Deno.testを使い、以下をカバーすること:
- CRUD操作の正常系
- バリデーションエラー
- 認証エラー
- ページネーション
テスト用のデータベースセットアップとクリーンアップも含めてください"
// tests/api/users_test.ts
import { assertEquals, assertExists } from "$std/assert/mod.ts";
const BASE_URL = "http://localhost:8000";
async function setupTestData() {
// テスト用データの投入
const response = await fetch(`${BASE_URL}/api/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "test@example.com",
name: "Test User",
role: "member",
}),
});
return await response.json();
}
Deno.test("GET /api/users - ユーザーリストを取得できること", async () => {
const response = await fetch(`${BASE_URL}/api/users`);
assertEquals(response.status, 200);
const body = await response.json();
assertExists(body.items);
assertExists(body.pagination);
assertEquals(typeof body.pagination.total, "number");
});
Deno.test("POST /api/users - ユーザーを作成できること", async () => {
const response = await fetch(`${BASE_URL}/api/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: `test-${Date.now()}@example.com`,
name: "New Test User",
role: "member",
}),
});
assertEquals(response.status, 201);
const user = await response.json();
assertExists(user.id);
assertEquals(user.name, "New Test User");
});
Deno.test("POST /api/users - バリデーションエラー", async () => {
const response = await fetch(`${BASE_URL}/api/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "invalid", name: "" }),
});
assertEquals(response.status, 400);
const body = await response.json();
assertEquals(body.error, "VALIDATION_ERROR");
});
Deno.test("GET /api/users - ページネーションが正しく動作すること", async () => {
const response = await fetch(`${BASE_URL}/api/users?page=1&limit=5`);
assertEquals(response.status, 200);
const body = await response.json();
assertEquals(body.pagination.page, 1);
assertEquals(body.pagination.limit, 5);
});
Deno Deployへのデプロイ:エッジで動くWebアプリケーション
デプロイ設定
gemini "Deno Freshプロジェクトのデプロイ戦略を策定してください。
以下を含むこと:
- Deno Deployの設定
- 環境変数管理
- GitHub Actionsワークフロー
- ヘルスチェック
- ロールバック手順"
GitHub Actions CI/CDワークフロー
# .github/workflows/deploy.yml
name: Deploy to Deno Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Check formatting
run: deno fmt --check
- name: Lint
run: deno lint
- name: Type check
run: deno check **/*.ts **/*.tsx
- name: Run tests
run: deno test -A --parallel
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: testdb
DB_USER: test
DB_PASSWORD: test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: denoland/deployctl@v1
with:
project: "fresh-dashboard"
entrypoint: "main.ts"
パフォーマンス最適化:Islands Architectureの真価を発揮する
Islandの最小化戦略
gemini "このDeno Freshプロジェクトのパフォーマンスを最適化してください。
以下の観点で分析・改善すること:
1. 不要なIslandの特定と静的コンポーネントへの変換
2. Islands内でのlazy import活用
3. Preactのシグナルによる状態管理の最適化
4. 画像の最適化
5. CSSの最小化"
パフォーマンス最適化のポイント
- Islandの粒度を最小化: ボタンのクリックハンドラだけが必要なら、ページ全体ではなくボタンだけをIsland化
- 遅延読み込み: Chart.js等の重いライブラリはIsland内で動的importする
- Signalsの活用: Preact Signalsを使うことで、不要な再レンダリングを防止
- SSRの活用: データ取得はサーバーサイドで行い、Islandにはpropsとして渡す
// routes/dashboard.tsx - サーバーサイドでデータ取得
import { Handlers, PageProps } from "$fresh/server.ts";
import Chart from "../islands/Chart.tsx";
interface DashboardData {
totalUsers: number;
activeUsers: number;
revenue: number;
}
export const handler: Handlers<DashboardData> = {
async GET(_req, ctx) {
// サーバーサイドでデータ取得
const data = await fetchDashboardData();
return ctx.render(data);
},
};
export default function Dashboard({ data }: PageProps<DashboardData>) {
return (
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">ダッシュボード</h1>
{/* 静的コンテンツ(JavaScriptなし) */}
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="bg-white rounded-lg shadow p-4">
<p class="text-gray-500">総ユーザー数</p>
<p class="text-3xl font-bold">{data.totalUsers.toLocaleString()}</p>
</div>
<div class="bg-white rounded-lg shadow p-4">
<p class="text-gray-500">アクティブユーザー</p>
<p class="text-3xl font-bold text-green-600">{data.activeUsers.toLocaleString()}</p>
</div>
<div class="bg-white rounded-lg shadow p-4">
<p class="text-gray-500">月間売上</p>
<p class="text-3xl font-bold text-blue-600">¥{data.revenue.toLocaleString()}</p>
</div>
</div>
{/* インタラクティブ部分だけIsland */}
<div class="grid grid-cols-2 gap-6">
<Chart
type="line"
apiEndpoint="/api/analytics/revenue"
title="売上推移"
/>
<Chart
type="bar"
apiEndpoint="/api/analytics/users"
title="ユーザー登録数"
/>
</div>
</div>
);
}
SES案件でのDeno Fresh活用シナリオ
適している案件
- スタートアップの新規開発: 高速な開発サイクルと軽量なインフラ
- 管理画面・ダッシュボード: SSR + Islandsで高速表示、必要な部分だけインタラクティブ
- APIサーバー: Denoの高いパフォーマンスとセキュリティ
- エッジコンピューティング: Deno Deployでの低レイテンシ配信
SES市場でのDeno需要(2026年)
- Deno経験者の月額単価: 65〜85万円(Node.jsエンジニアが+10〜15万円上乗せ可能)
- 求人増加率: 前年比200%増(ただし絶対数はNode.jsの5%程度)
- 主要採用企業: スタートアップ、テック企業のR&D部門
まとめ:Gemini CLI × Deno Freshで次世代Web開発を実践する
Gemini CLIを使ったDeno Fresh開発の要点をまとめます。
- ゼロビルドの開発体験: 設定不要でTypeScriptをそのまま実行、開発速度が大幅向上
- Islands Architectureの実践: インタラクティブな部分だけをクライアントサイドで実行し、パフォーマンスを最大化
- フルスタック統合: API Routes + SSR + Islandsで、バックエンドからフロントエンドまで一貫した開発
- エッジデプロイ: Deno Deployで世界中のエッジロケーションから高速配信
- Gemini CLIの活用: コンポーネント設計、API実装、テスト生成までAIが支援
先進技術であるDeno Freshのスキルは、SES市場での差別化要因になります。Gemini CLIを活用して、効率的にスキルを習得しましょう。