DenoとFreshでお手軽ポケモン図鑑開発

# はじめに

この記事は [NRI OpenStandia Advent Calendar 2024](https://qiita.com/advent-calendar/2024/nri-openstandia) の20日目の記事です。

最近、Node.jsの次世代ランタイムである[**Deno**](https://deno.com/)や[**Bun**](https://bun.sh/)について調査を進めていますが、ドキュメントを読むだけでは理解が難しいと感じることが多々あります。特に、Denoとそのフレームワークを使用したWebアプリケーション開発では、実際に手を動かしてみないと分からないことが多いです。
そこで、今回はDeno製のWebフレームワークである**Fresh**を使用して簡単なWebアプリケーションを開発してみることにしました。この取り組みを通じて、他のWebフレームワークでの開発との比較ができるようになればと考えています。

:::note info
**DenoとBunの調査記事は以下にありますので、ぜひ参考にしてください！**
https://zenn.dev/k4nd4/books/2142e58889cac9
:::

今回開発するWebアプリケーションはポケモン図鑑になります。このアプリケーションは、ポケモンの情報を手軽に検索・閲覧できるシンプルなWebツールです。ポケモンの図鑑番号を入力して検索する機能に加え、ランダムにポケモンを表示する機能や、すべてのポケモンを一覧で確認できる機能を備えています。
ポケモンの情報は[**PokeAPI**](https://pokeapi.co/)を使用しています。PokeAPIは、ポケモンに関する詳細なデータを提供するRESTful APIで、ポケモンの名前、画像、種族、タイプ、説明文など多岐にわたる情報をリアルタイムで取得することが可能です。これにより、アプリケーションをシンプルなコードで実現しつつ、常に最新のデータを提供できる仕組みを構築しました。

今回作成したソースコードは以下に格納しております。

https://github.com/kannoixia/deno-fresh-application

また今回のアプリケーションは以下にデプロイしています。

https://deno-fresh-pokemon-app-knym4y2d22n0.deno.dev/

:::note warn
デプロイの問題で上記URLが正常に動かない場合があります。
:::

# Denoとは

[**Deno**](https://deno.com/)は、JavaScriptやTypeScriptのための次世代ランタイムです。Node.jsの開発者であるライアン・ダール氏によって開発され、セキュリティや開発者体験の向上を目指して設計されました（Denoという名前もNodeを入れ替えたものです）。主な特徴として、以下の点が挙げられます。

1. **セキュア**  
   Denoは、ファイルシステムやネットワーク、環境変数へのアクセスが明示的に許可されない限りブロックされます。これにより、ランタイムのセキュリティが大幅に向上しています

2. **TypeScriptのネイティブサポート**  
   TypeScriptがランタイムレベルでサポートされており、トランスパイラや追加の設定を必要とせずに利用できます

3. **シンプルな依存関係管理**  
   Denoでは`package.json`や`node_modules`を必要とせず、URL経由で直接モジュールを取得します。このシンプルな仕組みにより、依存関係の管理が容易になります

4. **標準ライブラリの提供**  
   Denoには、ファイル操作やHTTPサーバーなど、よく使われる機能を含む標準ライブラリが同梱されています。これにより、外部パッケージに頼らずに多くの機能を実現できます

Denoは、これらの特徴を活かしつつ、モダンなWeb開発やスクリプト作成に適したランタイムとして注目を集めています。

# Freshとは

[**Fresh**](https://fresh.deno.dev/)は、Deno上で動作する最新のWebフレームワークです。シンプルで高速なWebアプリケーションの開発を目指して設計されており、主に以下の特徴を持っています。

1. **サーバーサイドレンダリング（SSR）の最適化**  
   Freshは、すべてのページをサーバーサイドでレンダリングし、必要最小限のJavaScriptをクライアントに送信することで、高速な読み込み速度と優れたユーザー体験を実現します

2. **自動的なコード分割**  
   各ページやコンポーネントは必要なコードだけをロードするように分割されるため、無駄のない効率的なリソース配信が可能です

3. **ネイティブのTypeScriptサポート**  
   FreshはTypeScriptを標準でサポートしており、追加の設定なしで型安全な開発が行えます

4. **統合されたリアクティブなUI**  
   Freshは、クライアントサイドのリアクティブなコンポーネントのためのシンプルな仕組みを提供し、サーバーとクライアントの間でスムーズな連携が可能です

5. **Deno標準のエコシステムとの連携**  
   FreshはDenoのエコシステムをフル活用し、依存関係管理がシンプルで、セキュリティやパフォーマンスが強化されています

Freshは、軽量で高性能なWebアプリケーションを構築するための新しい選択肢として、モダンなWeb開発者から注目されています。

# 開発環境のセットアップ

## Denoのインストール

下記のコマンドでDenoのインストールを行います。

```bash
# macOS、Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows
irm https://deno.land/install.ps1 | iex
```

下記コマンドでバージョンが表示されればインストール完了です。
本記事ではDeno v2.1.4を使用しました。

```bash
deno --version
deno 2.1.4 (stable, release, x86_64-pc-windows-msvc)
v8 13.0.245.12-rusty
typescript 5.6.2

```

:::note info
他のインストール方法は以下を参照してください。
https://docs.deno.com/runtime/
:::

## Freshプロジェクトの作成

下記のコマンドでダウンロードとFreshプロジェクトを作成します。

```bash
deno run -A -r https://fresh.deno.dev
```

ダウンロードが完了したらプロジェクト名と各種設定を行います。

```bash
🍋 Fresh: The next-gen web framework.

my-app　# プロジェクト名の入力

Do you want to use a styling library? [y/N] y #スタイリングライブラリを使用するか

1. tailwindcss (recommended)
2. Twind

1
Do you use VS Code? [y/N] y # VS Codeを使用するか
The manifest has been generated for 5 routes and 1 islands.

Project initialized!
```

作成したプロジェクトに移動して、下記コマンドでサーバーが起動します。

```bash
cd my-app
deno task start
```

サーバーが起動した状態で [http://localhost:8000/](http://localhost:8000/) にアクセスすると以下の画面が表示されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584046/1276d0c1-b693-0ba7-86c9-c72b1b96f450.png)

この画面が表示されれば、開発環境の構築は完了です。

# ディレクトリ構成

Freshプロジェクトのディレクトリ構造に関して簡単に説明します。
ディレクトリ構成は以下のようになっています。

```
初期プロジェクト                      今回作成するWebアプリケーション
　　　　　　　
├── components                        ├── components
│   └── Button.tsx                    │   └── (空)
├── islands                           ├── islands
│   └── Counter.tsx                   │   └── SearchForm.tsx
├── routes                            ├── routes
│   ├── index.tsx                     │   ├── api
│   ├── api                           │   │   ├── pokemon
│   │   └── joke.ts                   │   │   │   └── [id].ts
│   └── greet                         │   │   └── pokemon-list
│       └── [name].tsx                │   │       └── [page].ts
│                                     │   ├── pokemon
│                                     │   │   └── [id].tsx
│                                     │   └── pokemon-list
│                                     │       └── [page].tsx
├── static                            ├── static
│   ├── favicon.ico                   │   ├── pokeball.png
│   └── style.css                     │   └── style.css
├── dev.ts                            ├── dev.ts
├── fresh.gen.ts                      ├── fresh.gen.ts
├── main.ts                           ├── main.ts
└── deno.json                         ├── deno.json
                                      └── .env
```

ディレクトリの詳細は以下になります。

| ファイル/ディレクトリ        | 役割                                                                                                                                           |
|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
| `components/`              | 再利用可能なUIコンポーネントを格納するディレクトリ。例: ボタンやヘッダーなどの共有コンポーネント。                                              |
| `islands/`                 | インタラクティブなクライアントサイドのコンポーネントを格納するディレクトリ。部分的にJavaScriptやTypeScriptを必要とする機能を実装。                          |
| `routes/`                  | アプリケーションのルートを定義するディレクトリ。URLに対応するページコンポーネントを配置。                                                       |
| `routes/api/`              | APIエンドポイントを定義するためのディレクトリ。DenoでAPIを作成できる。例: `/api/joke` が `joke.ts` に対応。                                    |
| `static/`                  | 静的ファイルを配置するディレクトリ。例: 画像やフォント、アイコンなどが含まれる。                                                              |
| `dev.ts`                   | 開発サーバーを起動するためのエントリーポイント。通常 `deno task start` コマンドで実行される。                                                 |
| `fresh.gen.ts`             | Freshによって自動生成されるファイル。ルートやコンポーネントの構成を管理。手動編集は不要。                                                    |
| `deno.json`                | Denoプロジェクトの設定ファイル。タスク、パーミッション、インポートマップのパスなどを管理。                                                   |

この構成をもとに、必要に応じてコンポーネントやルートを追加し、プロジェクトを拡張していきます。

:::note info
**設定ファイルに関して**
Denoにはゼロコンフィグという考え方があります。ゼロコンフィグとは、追加の設定ファイルを必要とせずにシンプルに動作する設計思想を指します。

そのため、今回の Web アプリケーションの開発でも、環境変数を記入するための `.env` ファイル以外に設定ファイルを追加したり修正したりする必要はありませんでした。Fresh プロジェクト作成時に生成された `deno.json` もそのまま活用しています。
:::

# アプリケーション開発

## トップページの開発

まずはアプリケーションにアクセスして最初に表示されるトップページの開発を行います。
`routes/index.tsx`ファイルを修正します。

```typescript:routes/index.tsx
import SearchForm from "../islands/SearchForm.tsx";

export default function HomePage() {
  return (
    <div class="container">
      <h1>ポケモン図鑑</h1>
      <img src="pokeball.png" alt="Pokeball" class="pokeball" />
      <a href="/pokemon-list/1" class="button">ポケモン一覧</a>
      <SearchForm />
    </div>
  );
}
```

このコードでは、トップページに必要な要素を以下のように実装しています：

- **見出しとロゴ**: ページのタイトルとして「ポケモン図鑑」を表示し、ポケモンボールの画像をデザイン要素として配置しています
- **リンクボタン**: 「ポケモン一覧」というリンクボタンを配置し、全ポケモンのリストページ（`/pokemon-list/1`）への遷移を実現しています（詳細は[一覧表示ページ](#ポケモン一覧ページの開発)を参照してください）
- **検索フォームの追加**: `SearchForm`はislandsに属するインタラクティブなコンポーネントで、ユーザーがポケモンの検索やランダム検索を行える機能を提供しています

## 検索フォームの開発

ポケモンの検索やランダム検索を行う機能を提供する検索フォームの開発を行います。
`islands/SearchForm.tsx`として新しくファイルを作成します。

```typescript:islands/SearchForm.tsx
import { useState } from "preact/hooks";

export default function SearchForm() {
  const [pokedexNo, setPokedexNo] = useState("");

  const handleInput = (e: Event) => {
    const target = e.target as HTMLInputElement;
    setPokedexNo(target.value);
  };

  const randomSearch = () => {
    const randomNo = Math.floor(Math.random() * 1025) + 1;
    globalThis.location.href = `/pokemon/${randomNo}`;
  };

  const searchPokemon = () => {
    if (pokedexNo) {
      globalThis.location.href = `/pokemon/${pokedexNo}`;
    }
  };

  return (
    <div class="search-form">
      <input
        type="number"
        placeholder="図鑑No."
        value={pokedexNo}
        onInput={handleInput}
      />
      <button onClick={searchPokemon}>検索</button>
      <button onClick={randomSearch}>ランダム検索</button>
    </div>
  );
}
```

このコードでは、検索フォームを以下のように実装しています：

- **図鑑番号入力フォーム**: ユーザーがポケモンの図鑑番号を入力できるように、`<input>`要素を配置し、`useState`を使用してその入力値を管理しています
- **検索ボタン**: 入力された図鑑番号を基にポケモン詳細ページへ遷移する検索ボタンを実装しています
- **ランダム検索ボタン**: 特定の範囲でランダムに番号を選び、該当するポケモン詳細ページへ遷移するボタンを実装しています

Freshでは、ページ全体をサーバーサイドレンダリング（SSR）で生成し、必要な部分のみクライアントサイドでJavaScriptを実行させる「**islandsアーキテクチャ**」を採用しています。このアーキテクチャにより、パフォーマンスの向上と動的な機能の追加が実現できます。
今回のコードのように`onClick`や`useState`など、クライアント側で動的に動作する処理は`islands`ディレクトリまたは`islands`ディレクトリ内からimportされるファイルに記述しなければなりません。

:::note info
**islandsアーキテクチャ**は、ページの一部のみを動的にし、残りの部分は静的にレンダリングする手法です。これにより、Webページのパフォーマンスを向上させ、必要な部分だけにJavaScriptを適用します。

**特徴**：
- **サーバーサイドレンダリング（SSR）**: 初期表示はサーバー側で生成され、すぐに表示されます
- **部分的なJavaScript実行**: 必要な部分（例えばフォームやボタンなど）にのみ、クライアントサイドでJavaScriptを実行します
- **効率的な動的処理**: 動的な部分はブラウザ側で再レンダリング（hydration）されます

**メリット**：
- **高速なページ表示**: 静的な部分はすぐに表示され、動的な処理が後から適用されます
- **SEO向け最適化**: 静的コンテンツが優先されるため、SEOにも適しています
- **開発効率の向上**: 必要な部分だけに動的機能を追加でき、開発の自由度が増します

詳しくは以下の公式ブログを参照してください。
https://deno.com/blog/intro-to-islands
:::

ここまでの実装にCSSを当てることでトップ画面が表示されるはずです（今回の開発ではCSSは生成AIを使用して作成しました）。
`deno task start`でサーバーを起動し、[http://localhost:8000/](http://localhost:8000/) にアクセスして以下の画面が表示されることを確認してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584046/f176a7a7-e9cc-a2d8-4d4d-2bc4cb410541.png)

## API定義の開発

ポケモン検索結果ページ、ポケモン一覧ページで使用するAPI定義を開発します。

### ポケモン検索結果ページ用API定義

`routes/api/pokemon/[id].ts`として新しくファイルを作成します。

```typescript:routes/api/pokemon/[id].ts
// routes/api/pokemon/[id].ts
import { Handlers } from "$fresh/server.ts";

// ~~ interface定義は省略 ~~

export const handler: Handlers = {
  async GET(_, ctx) {
    const { id } = ctx.params;

    try {
      // ポケモン情報取得
      const pokemonRes = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
      if (!pokemonRes.ok) throw new Error("Pokemon not found");
      const pokemonData = await pokemonRes.json();

      // 名前（日本語名）取得
      const speciesUrl = pokemonData.species?.url;
      if (!speciesUrl) throw new Error("Species URL is undefined");
      const speciesRes = await fetch(speciesUrl);
      if (!speciesRes.ok) throw new Error("Species not found");
      const speciesData = await speciesRes.json();
      const japaneseName = speciesData.names.find((v: Name) => v.language.name === "ja")?.name;

      // タイプ取得
      const types = await Promise.all(
        pokemonData.types.map(async (typeObj: TypeObj) => {
          // URLにアクセスしてタイプデータを取得
          const typeRes = await fetch(typeObj.type.url);
          if (!typeRes.ok) throw new Error(`Failed to fetch type data for ${typeObj.type.name}`);
          const typeData = await typeRes.json();
          // 日本語名を取得
          const japaneseTypeName = typeData.names.find((v: Name) => v.language.name === "ja")?.name;

          return japaneseTypeName;
        })
      );

      // 分類取得
      const genus = speciesData.genera.find((v: Genera) => v.language.name === "ja")?.genus;

      // 説明文取得
      const flavorTextEntries = speciesData.flavor_text_entries;
      const preferredVersions = ["sword", "sun", "x"];
      let flavorText = "説明文が見つかりませんでした。";
      for (const version of preferredVersions) {
        // 日本語の説明文を探す
        const entry = flavorTextEntries.find(
          (v: FlavorTextEntry) => v.language.name === "ja" && v.version.name === version
        );
        if (entry) {
          flavorText = entry.flavor_text;
          break;
        }
      }


      return new Response(
        JSON.stringify({
          name: japaneseName,
          image: pokemonData.sprites.other["official-artwork"].front_default,
          types,
          genus,
          description: flavorText,
        }),
        { headers: { "Content-Type": "application/json" } }
      );
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "Unknown error";
      return new Response(JSON.stringify({ error: errorMessage }), { status: 500 });
    }
  },
};

```

このコードでは、ポケモンの詳細情報を取得するためのAPIエンドポイントを定義しています以下の内容で実装されています：

- **URL パラメータの取得**: `ctx.params` からポケモンのIDを取得し、そのIDを使ってポケモンの情報を外部API（PokeAPI）から取得します
- **ポケモン情報の取得**: PokeAPI からポケモンのデータを取得し、名前、種類、分類、説明文を日本語で取得します
  - **名前**: ポケモンの種族情報（`https://pokeapi.co/api/v2/pokemon-species`）から日本語名を取得します
  - **種類（タイプ）**: ポケモンのタイプごとに、対応するタイプデータをPokeAPIから取得し、その日本語名を取得します
  - **分類**: ポケモンの分類情報を取得します
  - **説明文**: ポケモンの説明文を、複数のバージョンから優先的に日本語で取得します
- **レスポンス**: 上記で取得した情報（名前、画像、タイプ、分類、説明文）をJSON形式でクライアントに返します
- **エラーハンドリング**: APIからのレスポンスが失敗した場合や、必要な情報が取得できない場合にはエラーメッセージを500ステータスコードで返します

この実装により、指定したIDのポケモン情報を日本語で取得し、クライアントに返すAPIが構築されています。

### ポケモン一覧ページ用API定義の開発

`routes/api/pokemon-list/[page].ts`として新しくファイルを作成します。

```typescript:routes/api/pokemon-list/[page].ts
// routes/api/pokemon/[id].ts
import { Handlers } from "$fresh/server.ts";

// ~~ interface定義は省略 ~~

export const handler: Handlers = {
  async GET(_, ctx) {
    const page = parseInt(ctx.params.page || "1");
    const limit = 100;
    const offset = (page - 1) * limit;

    try {
      // ポケモンリスト
      const response = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=${limit}&offset=${offset}`);
      if (!response.ok) throw new Error("Pokemon List not found");
      const responseData = await response.json();
      // リストデータの加工
      const transformedResults = await Promise.all(
        responseData.results.map(async (pokemon: { url: string }) => {
          const id = parseInt(pokemon.url.split("/")[6]);

          // idが10000を超える場合はスキップ
          if (id > 10000) {
            return null;
          }
          const speciesRes = await fetch(`https://pokeapi.co/api/v2/pokemon-species/${id}`);
          if (!speciesRes.ok) throw new Error("Species not found");
          const speciesData = await speciesRes.json();
          const japaneseName = speciesData.names.find(
            (v: Name) => v.language.name === "ja"
          )?.name;

          return { name: japaneseName || "不明", id };
        })
      );
      const filteredResults = transformedResults.filter(result => result !== null);

      return new Response(
        JSON.stringify({
          count: responseData.count,
          next: responseData.next,
          previous: responseData.previous,
          results: filteredResults,
        }),
        { headers: { "Content-Type": "application/json" } }
      );
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "Unknown error";
      return new Response(JSON.stringify({ error: errorMessage }), { status: 500 });
    }
  },
};

```

このコードでは、ポケモン一覧を取得するためのAPIエンドポイントを定義しています。以下の内容で実装されています：

- **ページネーションの実装**: `page` パラメータに基づき、ポケモンデータを1ページあたり100件で取得し、ページ番号に応じてデータをスライスしています
- **ポケモン一覧の取得**: `PokeAPI`からポケモンリストを取得し、その中から各ポケモンのIDを抽出します
  - **IDの取得**: 各ポケモンのURLからIDを取得し、`pokemon-species`エンドポイントを使って詳細情報を取得します
  - **日本語名の取得**: `pokemon-species`からポケモンの日本語名を取得します
- **データ整形とフィルタリング**: 取得したデータを`transformedResults`として加工し、IDが10000を超えるものは除外しています。その後、`null`値を除外した結果を返します
- **レスポンス**: 整形されたポケモンリスト、ページネーション情報（`count`, `next`, `previous`）を含んだJSON形式のレスポンスを返します
- **エラーハンドリング**: APIのレスポンスが失敗した場合、エラーメッセージを500ステータスコードで返します

この実装により、指定されたページに基づいてポケモンの一覧を日本語名で取得し、クライアントに返すAPIが構築されます。

## ポケモン検索結果ページの開発

トップ画面にて入力した図鑑番号のポケモンに関する詳細画面を開発します。
`routes/pokemon/[id].tsx`として新しくファイルを作成します。

```typescript:routes/pokemon/[id].tsx
import { Handlers, PageProps } from "$fresh/server.ts";

// ~~ interface定義、タイプ定義は省略 ~~

export const handler: Handlers = {
  async GET(_, ctx) {
    const { id } = ctx.params;
    const apiBaseUrl = Deno.env.get("API_BASE_URL") || "http://localhost:8000";
    const response = await fetch(`${apiBaseUrl}/api/pokemon/${id}`);
    if (!response.ok) {
      return ctx.render(null);
    }
    const data: PokemonData = await response.json();
    data.id = id;
    return ctx.render(data);
  },
};


export default function PokemonPage({ data }: PageProps<PokemonData>) {

  if (!data) {
    return (
      <div class="container">
        <h1>ポケモンが見つかりませんでした。</h1>
        <a href="/" class="button">戻る</a>
      </div>
    );
  }

  const cleanDescription = data.description
    .replace(/\n/g, ' ')
    .replace(/\s+/g, '')
    .trim();

  return (
    <div class="container">
      <h1 class="pokemon-id">図鑑No. {data.id}</h1>
      <h1>{data.name}</h1>
      <img src={data.image} alt={`${data.name}の画像`} class="pokemon-image" />
      <p>種族: {data.genus}</p>
      <p>
        タイプ: {data.types.map((type) => {
          const englishType = typeMap[type] || "unknown";
          return (
            <span class={`type type-${englishType}`} key={type}>
              {type}
            </span>
          );
        })}
      </p>
      <p>説明: {cleanDescription}</p>
      <a href="/" class="button">トップページへ戻る</a>
    </div>
  );
}

```

このコードでは、ポケモン検索結果ページに必要な要素を以下のように実装しています：

- **データ取得とエラーハンドリング**: 図鑑番号に基づいてAPIからポケモンの詳細データを取得します

- **ポケモンの詳細表示**: 図鑑番号、名前、画像、種族、タイプ、説明文を整形し、見やすい形で表示しています

- **トップページへのリンク**: ユーザーが簡単にトップページに戻れるよう、「トップページへ戻る」というリンクボタンを提供しています

トップページの検索フォームに「25」と入力すると、以下の画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584046/730634c5-de82-c078-7ee9-a8669c389395.png)

## ポケモン一覧ページの開発

ポケモン一覧を表示するページの開発を行います。
`routes/pokemon-list/[page].tsx`として新しくファイルを作成します。

```typescript:routes/pokemon-list/[page].tsx
import { Handlers, PageProps } from "$fresh/server.ts";

// ~~ interface定義は省略 ~~

export const handler: Handlers = {
  async GET(_, ctx) {
    const { page } = ctx.params;
    const apiBaseUrl = Deno.env.get("API_BASE_URL") || "http://localhost:8000";
    const response = await fetch(`${apiBaseUrl}/api/pokemon-list/${page}`);
    if (!response.ok) {
      return ctx.render(null);
    }
    const data: PokemonList = await response.json();
    data.page = parseInt(page);
    return ctx.render(data);
  },
};

export default function PokemonListPage({ data }: PageProps<PokemonList>) {
  const firstPokemonIndex = (data.page - 1) * 100 + 1;
  const lastPokemonIndex = Math.min(data.page * 100, parseInt(Deno.env.get("MAX_POKE_NUM") || "1025"));

  return (
    <div class="container">
      <div id="top"></div>
      <div class="pagination">
        <a
          href={`/pokemon-list/${data.page - 1}`}
          class={`button ${!data.previous ? "disabled" : ""}`}
          aria-disabled={!data.previous ? "true" : "false"}
        >
          前のページ
        </a>
        <a href="/" class="button">トップ画面に戻る</a>
        <a
          href={`/pokemon-list/${data.page + 1}`}
          class={`button ${!data.next ? "disabled" : ""}`}
          aria-disabled={!data.next ? "true" : "false"}
        >
          次のページ
        </a>
      </div>
      <h1>
        ポケモン一覧(ページ {data.page}) - No.{firstPokemonIndex} ~ {lastPokemonIndex}
      </h1>
      <table>
        <thead>
          <tr>
            <th>No.</th>
            <th>画像</th>
            <th>名前</th>
          </tr>
        </thead>
        <tbody>
          {data.results.map((pokemon) => (
            <tr key={pokemon.id}>
              <td>{pokemon.id}</td>
              <td>
                <img
                  src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`}
                  alt={`${pokemon.name}の画像`}
                  class="pokemon-list-image"
                />
              </td>
              <td>
                <a href={`/pokemon/${pokemon.id}`} class="pokemon-link">
                  {pokemon.name}
                </a>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <div class="bottom-navigation">
        <a href="#top" class="button">
          一番上に移動
        </a>
      </div>
    </div>
  );
}
```

このコードでは、ポケモン一覧ページに必要な要素を以下のように実装しています：

- **データ取得とページ分割**: 現在のページ番号に基づきAPIからポケモンのリストデータを取得し、ページ数に応じて表示範囲（ポケモンのNo.）を計算して動的に表示します

- **ページネーションの実装**: 「前のページ」と「次のページ」のナビゲーションボタンを設置し、データの有無に応じてボタンを有効化または無効化しています

- **ポケモンリストの表示**: 表形式でポケモンのNo.、画像、名前を一覧表示し、名前をリンクとして設定して各ポケモンの詳細ページへ遷移できるようにしています

- **トップへのナビゲーション**: ページの下部に「一番上に移動」ボタンを配置し、ユーザーが簡単にページ上部へ戻れるようにしています

トップページのポケモン一覧をクリックすると以下の画像が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584046/e6f1583e-c2db-d824-d9d9-51c76f95dfcc.png)

# デプロイ

最後に作成したアプリケーションをデプロイします。  
Denoには公式のサーバーレスホスティングサービス [Deno Deploy](https://deno.com/deploy)があり、簡単にアプリケーションをデプロイできます。  

Deno Deployは、Denoランタイム上で動作するアプリケーションをクラウド上で即座にホスティング可能なサービスです。  
GitHubリポジトリと連携することで、自動デプロイの設定も容易に行えます。また、エッジネットワークを利用して高速かつスケーラブルなアプリケーションの提供が可能です。  

デプロイ手順としては以下の通りです：  
1. **Deno Deployアカウントの作成**: [Deno Deployの公式サイト](https://deno.com/deploy) でアカウントを作成します。  
2. **プロジェクトの追加**: GitHubリポジトリを接続し、プロジェクトを作成します。  
3. **設定の確認**: デプロイ対象のブランチやエントリーポイント（`main.ts`など）を指定します。  
4. **デプロイ実行**: 設定を保存すると、自動的にアプリケーションがデプロイされます。  

これで、Deno Deployを使用してアプリケーションが世界中のユーザーに公開されます。
今回のポケモン図鑑も以下でデプロイしています。

https://deno-fresh-pokemon-app-knym4y2d22n0.deno.dev/

:::note warn
デプロイの問題で上記URLが正常に動かない場合があります。
:::


# まとめ

本記事では、DenoとFreshを使用してポケモン図鑑のWebアプリケーションを開発し、Deno Deployにデプロイするまでの一連の流れを紹介しました。開発環境の構築からデプロイまで、全体的にシンプルで直感的に進めることができました。

特に、DenoとFreshの組み合わせは、最小限の設定で動作するため、開発のスピードを大幅に向上させてくれました。フロントエンド開発に関しては初心者でしたが、Denoの柔軟性とFreshのシンプルさのおかげで、わずか1日程度で全行程を完了することができました。Deno Deployの環境にデプロイする過程も非常にスムーズで、ローカル環境から本番環境への移行も問題なく行えました。

このように、DenoとFreshを使えば、フロントエンド開発初心者でも短期間でアプリケーションを作成し、デプロイまで完了することができることを実感しました。今後もDenoを使った開発がより簡単で楽しくなることを期待しています。