学生の味方!!無料でフルスタックアプリをデプロイ!(React+Hono+D1+Cloudflare)


- [今回作成したアプリのソースコード](#今回作成したアプリのソースコード)
- [記事の概要](#記事の概要)
- [どこまで無料なのか](#どこまで無料なのか)
- [本記事の対象者](#本記事の対象者)
- [環境などの前提条件](#環境などの前提条件)
- [フロントエンドの作成](#フロントエンドの作成)
  - [初期設定](#初期設定)
  - [見た目部分の作成](#見た目部分の作成)
- [バックエンド&DBの作成](#バックエンドdbの作成)
  - [初期設定](#初期設定-1)
  - [DBの作成](#dbの作成)
  - [APIの作成](#apiの作成)
- [動作確認](#動作確認)
- [デプロイ](#デプロイ)
- [終わりに](#終わりに)

## 今回作成したアプリのソースコード

- [ソースコード](https://github.com/nightshrine/cdhr_article/tree/main)

ソースファイルは上記を参考にしながら、記事を読み進めて欲しい。

※デモも載せようと思ったが、Todoアプリであるため、一人が何か入力すると他の人にも見えてしまうのが危険だと思ったので公開を止めた。

:::note alert
本記事で作成するTodoアプリですが、デプロイした際は他の人も操作閲覧が可能ということなので、個人情報などを入力したりしないようにしましょう。また、セキュリティ的な観点で安全に作っているわけではないので、動作確認としてデプロイが終わったらすぐに削除することをお勧めします。
:::

自分なりにアレンジを加えて、オリジナルのアプリを作ってみてください。

## 記事の概要

学生は社会人と比べて時間はあることが多いが、お金はない…。エンジニアになりたいなら、デプロイをして他の人も見れるようにした方が良いと聞く…。Herokuの無料枠は終わってしまい、AWSなどにデプロイしようとするけど無料なのは最初の1年だけ…。

今回はそんなエンジニアを目指す学生のために、無料でフルスタックアプリをデプロイする方法を紹介する。使う技術としては、React、Hono、D1、Cloudflareを使う。D1はCloudflareサービス内で提供している DBaasで、SQLiteベースで動くためRDBとして使うことができる。

例として作成するアプリは簡単なTodoアプリである。見た目としては以下のようなとてもシンプルなものである。ただ、ここまでできれば色々と自分なりにアイデアを形にして、最終的にかなりのものが作れるようになるはずだ。

![Screenshot 2025-02-04 at 16.30.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/0e665c34-75ad-4651-bfc0-94a23468383d.png)

## どこまで無料なのか

Cloudflareは無料枠がかなり大きい。以下に詳細をまとめてみた（もし間違いなどがあれば、ご指摘いただきたい）

- Cloudflare Pages（Reactアプリをデプロイ）
  - リクエスト無制限
  - ビルド回数が、1ヶ月あたり500回まで
- Cloudflare Workers（Honoアプリをデプロイ）
  - リクエスト回数が、1日あたり10万回まで
  - 一回のリクエストあたり、CPUの最大稼働時間は10msまで
- KVストア(キーとバリューで保存できるストレージサービス)
  - リクエスト回数が、1日あたり10万回まで
  - 1日あたり1000回まで書き込み、削除、更新ができる
  - 最大容量は1GBまで
- D1
  - リクエスト回数が、1日あたり500万回まで
  - 1日あたり1000回まで書き込み、削除、更新ができる
  - 最大容量は5GBまで

細かいことは置いておいて、ここから分かるのは、学生が練習で使う分にはほぼ確実に無料枠を超えることはないということである。これで無料枠を超えるようなことがあれば、それはもう学生が作成するアプリとしては結構人気のアプリであると言えるのではないだろうか。

## 本記事の対象者

- 無料でフルスタックアプリをデプロイしたい方に向けての記事になるため、あまり凝った見た目や機能、設計などは考えていない
- HTML、CSS、JS(TS)、npm、ネットワークなどの基礎知識がある方を対象としており、そういった箇所の詳しい説明を省略している
- React、Hono、Cloudflareはなんとなく概要が分かるくらいで良い

## 環境などの前提条件

各種ツールのバージョンや開発環境

- PC: MacBook Air M2
- OS: macOS Sonoma 14.5
- VSCode: 1.96.3
- Vite: 6.0.5
- React: 18.3.1
- Hono: 4.6.0
- Node: 23.3.0
- Prisma: 6.3.0

以下のようなファイル構成で作成する

```console
.
├── backend
│   ...
└── frontend
    ...
```

## フロントエンドの作成

### 初期設定

まずは見た目部分を作ってしまう。ビルドツールはViteを使い、特に特別な理由がなければReactのTypeScript + SWCのテンプレートを使う。SWCはRustで作られている、JSとTSのバンドルとコンパイルを高速で行ってくれるものらしい。

以下のコマンドでプロジェクトを作成する。

```console
npm create vite@latest

✔ Project name: … frontend
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
```

作成したら、以下のコマンドで起動してみる。

```console
cd frontend
npm i
npm run dev
```

これで起動画面が立ち上がればOK。[http://localhost:5173](http://localhost:5173)にアクセスして確認すると、以下のような画面が表示される。

![Screenshot 2025-01-24 at 18.40.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/b7e4713d-f8e0-7183-961c-78b7f336d2b8.png)

ここからTodoアプリの見た目にしていく。

必須ではないのだが、importとしてルートディレクトリを@で指定できるようにしておく。

まずは`vite.config.ts`を以下のように修正する。

```console
npm i vite-tsconfig-paths
```

```typescript:vite.config.ts
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import tsConfigPaths from 'vite-tsconfig-paths';

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tsConfigPaths()], // <= tsConfigPathsを追加
});
```

tsConfigPathsを使うことで、typeScriptの設定をviteでそのまま流用できるようになる。

次に、`tsconfig.app.json`を以下のように修正する。

```json:tsconfig.app.json
{
  "compilerOptions": {
    ...,
    ...,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
```

これで`import`の際に`@`でルートディレクトリを指定できるようになる。

そして今回は、APIへの通信を行うと言うことで、エラーハンドリングなど色々便利なTanStack Queryを使っていく。

```console
npm i @tanstack/react-query
```

次にmain.tsxを以下のように修正する。ちなみにindex.cssは不要なので消している。

```tsx:src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';

const queryClient = new QueryClient();

createRoot(document.getElementById('root')!).render(
    <StrictMode>
        <QueryClientProvider client={queryClient}>
            <App />
        </QueryClientProvider>
    </StrictMode>
);

```

実際に使用するときは以下のように使用する。

```tsx:src/App.tsx
...
function App() {
    ...
    const todosQueryResult = useQuery({
        queryKey: ['initTodos'],
        queryFn: TodoService.getTodos,
    });
...
```

### 見た目部分の作成

ファイルを一つ一つ見せると長くなるので、ざっくりとした説明をする。詳細は[ソースコード](https://github.com/nightshrine/cdhr_article/tree/main)を参照してほしい。

srcディレクトリ配下の構成としては以下のようなものである。それぞれ説明を後述する。

```console
src
├── App.css
├── App.tsx               // ①
├── assets
│   └── react.svg
├── constants
│   └── ApiConstants.ts   // ②
├── definitions
│   └── Todo.ts           // ③
├── main.tsx
├── services
│   ├── ApiService.ts     // ④
│   ├── GetEnvService.ts  // ⑤
│   └── TodoService.ts    // ⑥
└── vite-env.d.ts
```

- ①: Todoアプリのメイン部分。本アプリでのコンポーネントはこれだけである。
- ②: APIのエンドポイントなどを定義する。これはローカル環境のURLを定義している。
- ③: Todoの型定義を行う。APIのレスポンスなどで使う。
- ④: APIのリクエストを行う動作を抽象化したもの。
- ⑤: 環境変数を取得するためのもの。今回はデプロイ後のAPIのエンドポイントを取得するために使う。
- ⑥: Todoリスト関連のAPIを叩くためのもの。

これで見た目の部分は完成である。まだバックエンドを作ってないので完全には動かないが、一応見た目だけであれば動作確認ができる（追加や削除などはできない）。

以下のようにコメントアウトなどを駆使して、APIを叩かないようにすれば良い。

```tsx:src/App.tsx
import { Todo } from '@/definitions/Todo';
import { TodoService } from '@/services/TodoService';
import { useState } from 'react';
import './App.css';

function App() {
    const [inputTodo, setInputTodo] = useState('');
    const [todos, setTodos] = useState<Todo[]>([
        {
            id: 0,
            name: '直打ちで追加したTodo',
            done: false,
        },
    ]);

    // const todosQueryResult = useQuery({
    //     queryKey: ['initTodos'],
    //     queryFn: TodoService.getTodos,
    // });

    // if (todosQueryResult.isLoading) {
    //     return <div>Loading...</div>;
    // }

    // if (todosQueryResult.isError) {
    //     return <div>Error</div>;
    // }

    // if (!todosQueryResult.data) {
    //     return <div>No data</div>;
    // }

    // if (!isLoaded) {
    //     setTodos(todosQueryResult.data);
    //     setIsLoaded(true);
    // }

    ...
}
```

![Screenshot 2025-02-04 at 17.45.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/f6651754-3f40-0834-72a2-6bd0ef211336.png)

## バックエンド&DBの作成

### 初期設定

次にTodoリストの取得、追加、削除を行うバックエンドを作成していく。まずはDBを作成してしまってから、APIを作成していく。

まずはReactと同じようにしてHonoを起動させる。今後進めていく上でCloudflareのアカウントを持っていない場合は、ログインなどを求めれるはずだ。その際は、別サイトなどを参考にしてほしい。

```console
npm create hono@latest

? Target directory backend
? Which template do you want to use? cloudflare-workers
? Do you want to install project dependencies? yes
? Which package manager do you want to use? npm
```

これで`backend`ディレクトリが作成される。`backend`ディレクトリに移動して、以下のコマンドでHonoを起動させる。

```console
cd backend
npm run dev
```

これで、[http://localhost:8787](http://localhost:8787)にアクセスして、以下のような画面が表示されればOK。

![Screenshot 2025-02-04 at 18.00.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/e010fe25-fd43-2b3b-85bc-ddddfa31dfe1.png)

### DBの作成

次はPrismaを使ってDBを作成していく。PrismaはORMの一つで、DBの作成と操作を簡単に行える。また、Prisma studioというGUIツールを使うことで、ブラウザ上でDBの中身を変更することもできる。

まずは以下のコマンドでPrismaをインストールする。

```console
cd backend
npm i prisma --save-dev
```

それに加えて、`@prisma/client`もインストールしておく必要がある。これによって、TSでORMとしてPrismaを使うことができる。`prisma generate`を実行することで、TS上で方安全なコードを記述することが可能になる。

もちろんDBとしてD1を使うので、`@prisma/adapter-d1`もインストールしておく。

```console
npm install @prisma/client @prisma/adapter-d1
```

次に以下のコマンドでPrismaの設定ファイルを作成する。

```console
npx prisma init
```

これでprismaの設定ファイルが作成される。`prisma/schema.prisma`を以下のように修正する。

```prisma:prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
  previewFeatures = ["driverAdapters"]  // この行を追加
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

// Todoのテーブルを追加
model todo {
  id    Int     @id @default(autoincrement())
  name  String
  done  Boolean
}
```

これによってマイグレーションファイルを自動生成でき、DBをスムーズに作成できる。
ちなみに詳しい情報については、[Prisma on Cloudflare](https://hono.dev/examples/prisma)を参照してほしい。

まずはデータベースとなるD1を作成する。

```console
npx wrangler d1 create __DATABASE_NAME__
```

`__DATABASE_NAME__`には任意の名前を入れる。私は`tutorial_d1`とした。すると、wrangler.json（もしくはwrangler.toml）に以下のような記述があるはずだ。

```json:wrangler.json
{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "tutorial_d1",
      "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // ここにIDが入る
    }
  ]
}
```

これでD1は作成されたので、`prisma/schema.prisma`を元にマイグレーションファイルを作成する。

```console
npx prisma migrate diff \
  --from-empty \
  --to-schema-datamodel ./prisma/schema.prisma \
  --script \
  --output migrations/0001_create_todo_table.sql
```

これで、`0001_create_todo_table.sql`というマイグレーションファイルが、`migrations`ディレクトリに作成される。中身を見ると以下のようになっている。

```sql:migrations/0001_create_todo_table.sql
-- CreateTable
CREATE TABLE "todo" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "name" TEXT NOT NULL,
    "done" BOOLEAN NOT NULL
);
```

これを反映させればD1内にテーブルが作られる。以下のコマンドでマイグレーションファイルを適用する。

```console
npx wrangler d1 migrations apply __DATABASE_NAME__ --local
```

これでローカルのD1内にテーブルが作成される。ちなみに、localをremoteに変えることで本番環境に適用できる。

ここでしっかり動作しているか確かめるために、Prisma studioを使ってみる。以下のコマンドでPrisma studioを起動する。

```console
npx prisma studio
```

これで、[http://localhost:5555](http://localhost:5555)にアクセスすると、以下のような画面が表示される。

![Screenshot 2025-02-04 at 18.55.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/86584104-ba73-310d-08e3-8ea4b08c6cec.png)

`Add Record`と書かれた部分を押せばデータ追加もできる。これでDBの作成は完了である。

### APIの作成

次にAPIを作成していく。とはいっても全部で3ファイルほどしか作成してないので、一気に説明してしまう。
まずは`src/`ディレクトリに`Bindings.ts`を作成し、以下のように記述する。

```typescript:src/Bindings.ts
export type Bindings = {
    DB: D1Database;
};
```

これはBindingsはwrangler.jsonで設定した値などを適用するためのものなのだが、その型ファイルである。

次に同じ階層に`getPrismaClient.ts`を作成し、以下のように記述する。

```typescript:src/getPrismaClient.ts
const { PrismaClient } = await import('@prisma/client');
const { PrismaD1 } = await import('@prisma/adapter-d1');

export const getPrismaClient = async (db: D1Database) => {
    const adapter = new PrismaD1(db);
    return new PrismaClient({ adapter });
};
```

これでDBからデータを取るための関数取得の部分を共通化できた。

最後に`src/index.ts`を以下のように修正する。

```typescript:src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { Bindings } from './Bindings';
import { getPrismaClient } from './getPrismaClient';

const app = new Hono<{ Bindings: Bindings }>();

app.use(
    '/*',
    cors({
        origin: '*',
    })
);

app.get('/todos', async (c) => {
    const prisma = await getPrismaClient(c.env.DB);

    const todos = await prisma.todo.findMany();
    return c.json(todos);
});

app.post('/todos', async (c) => {
    const { name, done } = await c.req.json();

    const prisma = await getPrismaClient(c.env.DB);

    const todo = await prisma.todo.create({
        data: {
            name,
            done,
        },
    });

    return c.json(todo);
});

app.put('/todos/:id/toggle', async (c) => {
    const prisma = await getPrismaClient(c.env.DB);

    const todo = await prisma.todo.findUnique({
        where: { id: parseInt(c.req.param('id')) },
    });

    await prisma.todo.update({
        where: { id: parseInt(c.req.param('id')) },
        data: {
            done: todo?.done ? false : true,
        },
    });

    return c.json(todo);
});

app.delete('/todos/:id', async (c) => {
    const prisma = await getPrismaClient(c.env.DB);

    await prisma.todo.delete({
        where: { id: parseInt(c.req.param('id')) },
    });

    return c.json({ message: 'Todo deleted' });
});

export default app;
```

これでAPI部分の作業は完了である。ちなみにTodoリスト作成でよくある、Todoの内容を書き換える仕組みは作っていない。フロント部分も合わせて追加で作成してみると理解が深まるかもしれない。

## 動作確認

それでは早速、ローカルにおいて動作確認を行なってみる。

バックエンドとフロントエンドを起動させる。

```console
cd backend
npm run dev
cd ../frontend
npm run dev
```

これで、[http://localhost:5173](http://localhost:5173)にアクセスすれば、Todoリストっぽいことができるようになっているはずだ。

![Screenshot 2025-02-04 at 19.16.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/b240ae17-4029-4c01-24d7-4116f31e7ede.png)

## デプロイ

ついにこの時が来た。デプロイを行って、他の人にも見せることができるようにする。

まずは Cloudflare workersとしてバックエンドをデプロイする。以下のコマンドでデプロイを行う。

```console
cd backend
npm run build
```

この時、デプロイ先のURLが表示されるので、後ですぐに確認できるようにしておく。

次に、さっきローカルで作成したD1を今回はリモートに適用する。

```console
npx wrangler d1 migrations apply __DATABASE_NAME__ --remote
```

これで、D1にテーブルが作成される。最後にフロントエンドだが、これは他と比べて少しだけ面倒である。まずこのアプリ自体をgitにpushしておく。そしてCloudflare Pagesのページに行って、git上のリポジトリを選択してビルド設定を行う。

設定は以下のように行えばよい。環境変数としてAPIのエンドポイントを設定しておけば、デプロイしたフロントエンドアプリがバックエンドのAPIを叩くことができる。

![Screenshot 2025-02-04 at 19.23.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/e1bf9080-1b0f-a0ab-bf7a-7daf08297ab1.png)

これでデプロイが勝手に行われる。しかも嬉しいことにGitと連携しているので、Git側で何か変更があれば自動で再デプロイしてくれる。

今回はやらないが、バックエンド側もCICDを使って自動デプロイを行うことができれば、さらに開発効率を上げることができるだろう。

数分待った後でフロントエンドのデプロイ先URLにアクセスすれば、ローカルで確認できた同じ動作が確認できるだろう。

おめでとう！！！

## 終わりに

こんなに手早く簡単に、かつ無料でフルスタックアプリを作れるというのは本当にありがたい。無料でも、これで出来ることの幅が大分広がったことは間違いないだろう。もっと拡張していけば、Auth0を使って認証機能を追加したSNSアプリや、予約システム、チャットアプリなど、様々なアイデアを形にできる。

そもそもどんなスキルにおいても、能力を伸ばす一番有効な手段がアウトプットであると思っている（人に教えるということもあり）。どんどんアウトプットして、少しずつ新しい技術を取り入れていけば、エンジニアとしてのスキルは確実に上がっていくし、何より楽しく開発ライフを送ることができる。かつそれがデプロイできれば、他の人へ分かりやすくスキルを伝えることができる。

ちなみに私は今回のような技術構成でポートフォリオを作成しているのだが、D1にマスタデータとして内容を保存しておけば、内容が更新されるたびにいちいち編集してデプロイし直す必要はない。D1の中身を変えるだけで反映される。ポートフォリオは更新頻度が高いので、とても便利だと思っている。
もしよかったら[私のポートフォリオ](https://portfolio-cdhr.pages.dev/)をぜひ見ていってほしい。

~ 最後まで読んでいただき、ありがとうございました ~