Deno Fresh フレームワーク入門
Deno Freshは、Deno向けの次世代Webフレームワークです。Islands Architecture、ゼロビルド構成、エッジでの実行など、モダンなWeb開発の課題を解決する設計が特徴です。
Fresh の特徴
1. Islands Architecture
ページ全体をサーバーサイドレンダリング(SSR)し、インタラクティブな部分だけをクライアントサイドのJavaScriptとして「島(Island)」のように配置する設計思想です。
┌─────────────────────────────┐
│ Static HTML (SSR) │
│ ┌──────┐ ┌─────────┐ │
│ │Island│ │ Island │ │
│ │ JS │ │ JS │ │
│ └──────┘ └─────────┘ │
│ │
│ ┌──────────────┐ │
│ │ Island │ │
│ │ JS │ │
│ └──────────────┘ │
└─────────────────────────────┘
メリット:
- 必要最小限のJavaScriptのみ配信
- 高速な初期ロード
- SEOに優れる
- パフォーマンスが自動的に最適化される
2. ゼロビルド構成
ビルドステップが不要で、TypeScript、JSXを直接実行できます。
// routes/index.tsx - ビルド不要でそのまま動く
export default function Home() {
return (
<div>
<h1>Hello Fresh!</h1>
</div>
);
}
3. エッジ対応
Deno Deployなどのエッジランタイムで動作し、低レイテンシーを実現します。
プロジェクトのセットアップ
インストール
# Denoのインストール(未インストールの場合)
curl -fsSL https://deno.land/x/install/install.sh | sh
# Freshプロジェクトの作成
deno run -A -r https://fresh.deno.dev my-app
cd my-app
プロジェクト構造
my-app/
├── deno.json # Deno設定
├── dev.ts # 開発サーバー
├── main.ts # 本番エントリーポイント
├── fresh.gen.ts # 自動生成ファイル
├── routes/ # ルーティング
│ ├── index.tsx # /
│ ├── about.tsx # /about
│ └── api/
│ └── joke.ts # /api/joke
├── islands/ # インタラクティブコンポーネント
│ └── Counter.tsx
├── components/ # 静的コンポーネント
│ └── Button.tsx
└── static/ # 静的ファイル
└── logo.svg
開発サーバー起動
deno task start
http://localhost:8000 でアクセス可能になります。
ルーティング
Freshはファイルベースルーティングを採用しています。
基本的なページ
// routes/index.tsx
import { Head } from "$fresh/runtime.ts";
export default function Home() {
return (
<>
<Head>
<title>Fresh App</title>
</Head>
<div class="p-4 mx-auto max-w-screen-md">
<h1 class="text-4xl font-bold">Welcome to Fresh</h1>
<p class="my-4">
This is a server-side rendered page.
</p>
</div>
</>
);
}
動的ルーティング
// routes/users/[id].tsx
import { PageProps } from "$fresh/server.ts";
export default function UserPage(props: PageProps) {
const { id } = props.params;
return (
<div>
<h1>User ID: {id}</h1>
</div>
);
}
アクセス例:
/users/123→id = "123"/users/alice→id = "alice"
Catch-all ルート
// routes/docs/[...slug].tsx
import { PageProps } from "$fresh/server.ts";
export default function DocsPage(props: PageProps) {
const { slug } = props.params;
// slug は配列として渡される
return (
<div>
<h1>Docs: {slug}</h1>
</div>
);
}
アクセス例:
/docs/intro→slug = "intro"/docs/guide/getting-started→slug = "guide/getting-started"
データの取得
Handlers
サーバーサイドでデータを取得してページに渡します。
// routes/products/[id].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface Product {
id: string;
name: string;
price: number;
}
export const handler: Handlers<Product> = {
async GET(req, ctx) {
const { id } = ctx.params;
// データベースやAPIから取得
const product = await fetchProduct(id);
if (!product) {
return ctx.renderNotFound();
}
return ctx.render(product);
},
};
export default function ProductPage({ data }: PageProps<Product>) {
return (
<div>
<h1>{data.name}</h1>
<p>価格: ¥{data.price.toLocaleString()}</p>
</div>
);
}
async function fetchProduct(id: string): Promise<Product | null> {
// 実際のデータ取得ロジック
return {
id,
name: "Sample Product",
price: 1000,
};
}
複数のHTTPメソッド対応
// routes/api/items.ts
import { Handlers } from "$fresh/server.ts";
interface Item {
id: string;
title: string;
}
const items: Item[] = [];
export const handler: Handlers = {
// GET /api/items
GET(req) {
return new Response(JSON.stringify(items), {
headers: { "Content-Type": "application/json" },
});
},
// POST /api/items
async POST(req) {
const body = await req.json();
const newItem: Item = {
id: crypto.randomUUID(),
title: body.title,
};
items.push(newItem);
return new Response(JSON.stringify(newItem), {
status: 201,
headers: { "Content-Type": "application/json" },
});
},
// DELETE /api/items
DELETE(req) {
items.length = 0;
return new Response(null, { status: 204 });
},
};
Islands (インタラクティブコンポーネント)
islands/ ディレクトリ内のコンポーネントは、クライアント側で実行されます。
カウンターの例
// islands/Counter.tsx
import { Signal, useSignal } from "@preact/signals";
interface CounterProps {
start: number;
}
export default function Counter(props: CounterProps) {
const count = useSignal(props.start);
return (
<div class="flex gap-2 w-full">
<p class="flex-grow-1 font-bold text-xl">{count.value}</p>
<button
class="px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => count.value++}
>
+1
</button>
<button
class="px-4 py-2 bg-red-500 text-white rounded"
onClick={() => count.value--}
>
-1
</button>
</div>
);
}
ページで使用
// routes/index.tsx
import Counter from "../islands/Counter.tsx";
export default function Home() {
return (
<div>
<h1>Counter Example</h1>
<Counter start={0} />
</div>
);
}
フォーム入力の例
// islands/SearchBox.tsx
import { useSignal } from "@preact/signals";
export default function SearchBox() {
const query = useSignal("");
const results = useSignal<string[]>([]);
const handleSearch = async () => {
const res = await fetch(`/api/search?q=${query.value}`);
const data = await res.json();
results.value = data;
};
return (
<div>
<input
type="text"
value={query.value}
onInput={(e) => query.value = e.currentTarget.value}
class="border px-4 py-2"
placeholder="Search..."
/>
<button
onClick={handleSearch}
class="ml-2 px-4 py-2 bg-blue-500 text-white rounded"
>
Search
</button>
<ul class="mt-4">
{results.value.map((result) => (
<li key={result}>{result}</li>
))}
</ul>
</div>
);
}
Preact Signals
Freshは状態管理にPreact Signalsを使用します。
基本的な使い方
import { useSignal, computed, effect } from "@preact/signals";
export default function SignalsDemo() {
const count = useSignal(0);
const doubled = computed(() => count.value * 2);
// 副作用
effect(() => {
console.log(`Count is now: ${count.value}`);
});
return (
<div>
<p>Count: {count.value}</p>
<p>Doubled: {doubled.value}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
グローバル状態
// signals/store.ts
import { signal } from "@preact/signals";
export interface User {
id: string;
name: string;
}
export const currentUser = signal<User | null>(null);
export const isLoggedIn = computed(() => currentUser.value !== null);
使用例:
// islands/UserProfile.tsx
import { currentUser, isLoggedIn } from "../signals/store.ts";
export default function UserProfile() {
if (!isLoggedIn.value) {
return <p>Please log in</p>;
}
return (
<div>
<h2>Welcome, {currentUser.value?.name}</h2>
<button onClick={() => currentUser.value = null}>
Log out
</button>
</div>
);
}
ミドルウェア
リクエストの前処理・後処理を行います。
// routes/_middleware.ts
import { MiddlewareHandlerContext } from "$fresh/server.ts";
interface State {
user?: {
id: string;
name: string;
};
}
export async function handler(
req: Request,
ctx: MiddlewareHandlerContext<State>,
) {
// 認証チェック
const authHeader = req.headers.get("Authorization");
if (authHeader) {
const user = await validateToken(authHeader);
if (user) {
ctx.state.user = user;
}
}
// レスポンスを取得
const resp = await ctx.next();
// レスポンスヘッダーを追加
resp.headers.set("X-Custom-Header", "value");
return resp;
}
async function validateToken(token: string) {
// トークン検証ロジック
return { id: "123", name: "Alice" };
}
特定ルートのミドルウェア
// routes/admin/_middleware.ts
export async function handler(req: Request, ctx: MiddlewareHandlerContext) {
const { user } = ctx.state;
if (!user || !user.isAdmin) {
return new Response("Forbidden", { status: 403 });
}
return await ctx.next();
}
レイアウト
共通レイアウトを定義します。
// routes/_layout.tsx
import { PageProps } from "$fresh/server.ts";
export default function Layout({ Component, state }: PageProps) {
return (
<div class="layout">
<header class="bg-blue-500 text-white p-4">
<nav class="container mx-auto flex gap-4">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main class="container mx-auto p-4">
<Component />
</main>
<footer class="bg-gray-800 text-white p-4 text-center">
© 2025 My App
</footer>
</div>
);
}
スタイリング
Twind (Tailwind CSS)
FreshはデフォルトでTwindを使用します。
// routes/index.tsx
export default function Home() {
return (
<div class="min-h-screen bg-gray-100">
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-blue-600 mb-4">
Hello Fresh
</h1>
<button class="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition">
Click me
</button>
</div>
</div>
);
}
カスタムCSS
// static/styles.css
.custom-button {
background: linear-gradient(45deg, #ff6b6b, #ee5a6f);
color: white;
padding: 12px 24px;
border-radius: 8px;
border: none;
cursor: pointer;
}
// routes/_app.tsx
import { AppProps } from "$fresh/server.ts";
export default function App({ Component }: AppProps) {
return (
<html>
<head>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Component />
</body>
</html>
);
}
APIルート
RESTful APIを簡単に構築できます。
// routes/api/users/[id].ts
import { Handlers } from "$fresh/server.ts";
interface User {
id: string;
name: string;
email: string;
}
const users = new Map<string, User>();
export const handler: Handlers = {
GET(req, ctx) {
const { id } = ctx.params;
const user = users.get(id);
if (!user) {
return new Response("Not Found", { status: 404 });
}
return Response.json(user);
},
async PUT(req, ctx) {
const { id } = ctx.params;
const body = await req.json();
const user: User = {
id,
name: body.name,
email: body.email,
};
users.set(id, user);
return Response.json(user);
},
DELETE(req, ctx) {
const { id } = ctx.params;
users.delete(id);
return new Response(null, { status: 204 });
},
};
データベース連携
Deno KVの使用
// routes/api/todos.ts
import { Handlers } from "$fresh/server.ts";
const kv = await Deno.openKv();
interface Todo {
id: string;
title: string;
completed: boolean;
}
export const handler: Handlers = {
async GET() {
const entries = kv.list<Todo>({ prefix: ["todos"] });
const todos: Todo[] = [];
for await (const entry of entries) {
todos.push(entry.value);
}
return Response.json(todos);
},
async POST(req) {
const body = await req.json();
const id = crypto.randomUUID();
const todo: Todo = {
id,
title: body.title,
completed: false,
};
await kv.set(["todos", id], todo);
return Response.json(todo, { status: 201 });
},
};
デプロイ
Deno Deploy
# Deno Deployへのデプロイ
deno install -Arf jsr:@deno/deployctl
deployctl deploy
Docker
# Dockerfile
FROM denoland/deno:latest
WORKDIR /app
COPY . .
RUN deno cache main.ts
EXPOSE 8000
CMD ["deno", "run", "-A", "main.ts"]
# ビルド・実行
docker build -t fresh-app .
docker run -p 8000:8000 fresh-app
パフォーマンス最適化
1. 画像最適化
// components/OptimizedImage.tsx
interface ImageProps {
src: string;
alt: string;
width: number;
height: number;
}
export function OptimizedImage({ src, alt, width, height }: ImageProps) {
return (
<img
src={src}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
/>
);
}
2. コンポーネントの遅延ロード
// islands/LazyComponent.tsx
import { lazy } from "preact/compat";
const HeavyComponent = lazy(() => import("../components/HeavyComponent.tsx"));
export default function LazyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
まとめ
Deno Freshは、Islands Architectureとゼロビルド構成により、高速で開発体験の良いWebアプリケーションを実現するフレームワークです。
主な特徴:
- Islands Architectureによる最適化されたJavaScript配信
- ビルドステップ不要の高速な開発体験
- エッジランタイムでの実行
- Preact Signalsによる効率的な状態管理
- ファイルベースルーティング
特に、パフォーマンスが重要なWebアプリケーション、SEOが必要なサイト、エッジでの実行が求められるプロジェクトに最適です。Denoエコシステムの強力なツール群と組み合わせることで、モダンなWeb開発を効率的に進められます。