今回作成したアプリのソースコード
ソースファイルは上記を参考にしながら、記事を読み進めて欲しい。
※デモも載せようと思ったが、Todoアプリであるため、一人が何か入力すると他の人にも見えてしまうのが危険だと思ったので公開を止めた。
本記事で作成するTodoアプリですが、デプロイした際は他の人も操作閲覧が可能ということなので、個人情報などを入力したりしないようにしましょう。また、セキュリティ的な観点で安全に作っているわけではないので、動作確認としてデプロイが終わったらすぐに削除することをお勧めします。
自分なりにアレンジを加えて、オリジナルのアプリを作ってみてください。
記事の概要
学生は社会人と比べて時間はあることが多いが、お金はない…。エンジニアになりたいなら、デプロイをして他の人も見れるようにした方が良いと聞く…。Herokuの無料枠は終わってしまい、AWSなどにデプロイしようとするけど無料なのは最初の1年だけ…。
今回はそんなエンジニアを目指す学生のために、無料でフルスタックアプリをデプロイする方法を紹介する。使う技術としては、React、Hono、D1、Cloudflareを使う。D1はCloudflareサービス内で提供している DBaasで、SQLiteベースで動くためRDBとして使うことができる。
例として作成するアプリは簡単なTodoアプリである。見た目としては以下のようなとてもシンプルなものである。ただ、ここまでできれば色々と自分なりにアイデアを形にして、最終的にかなりのものが作れるようになるはずだ。
どこまで無料なのか
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
以下のようなファイル構成で作成する
.
├── backend
│ ...
└── frontend
...
フロントエンドの作成
初期設定
まずは見た目部分を作ってしまう。ビルドツールはViteを使い、特に特別な理由がなければReactのTypeScript + SWCのテンプレートを使う。SWCはRustで作られている、JSとTSのバンドルとコンパイルを高速で行ってくれるものらしい。
以下のコマンドでプロジェクトを作成する。
npm create vite@latest
✔ Project name: … frontend
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
作成したら、以下のコマンドで起動してみる。
cd frontend
npm i
npm run dev
これで起動画面が立ち上がればOK。http://localhost:5173にアクセスして確認すると、以下のような画面が表示される。
ここからTodoアプリの見た目にしていく。
必須ではないのだが、importとしてルートディレクトリを@で指定できるようにしておく。
まずはvite.config.ts
を以下のように修正する。
npm i vite-tsconfig-paths
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
を以下のように修正する。
{
"compilerOptions": {
...,
...,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
これでimport
の際に@
でルートディレクトリを指定できるようになる。
そして今回は、APIへの通信を行うと言うことで、エラーハンドリングなど色々便利なTanStack Queryを使っていく。
npm i @tanstack/react-query
次にmain.tsxを以下のように修正する。ちなみにindex.cssは不要なので消している。
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>
);
実際に使用するときは以下のように使用する。
...
function App() {
...
const todosQueryResult = useQuery({
queryKey: ['initTodos'],
queryFn: TodoService.getTodos,
});
...
見た目部分の作成
ファイルを一つ一つ見せると長くなるので、ざっくりとした説明をする。詳細はソースコードを参照してほしい。
srcディレクトリ配下の構成としては以下のようなものである。それぞれ説明を後述する。
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を叩かないようにすれば良い。
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);
// }
...
}
バックエンド&DBの作成
初期設定
次にTodoリストの取得、追加、削除を行うバックエンドを作成していく。まずはDBを作成してしまってから、APIを作成していく。
まずはReactと同じようにしてHonoを起動させる。今後進めていく上でCloudflareのアカウントを持っていない場合は、ログインなどを求めれるはずだ。その際は、別サイトなどを参考にしてほしい。
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を起動させる。
cd backend
npm run dev
これで、http://localhost:8787にアクセスして、以下のような画面が表示されればOK。
DBの作成
次はPrismaを使ってDBを作成していく。PrismaはORMの一つで、DBの作成と操作を簡単に行える。また、Prisma studioというGUIツールを使うことで、ブラウザ上でDBの中身を変更することもできる。
まずは以下のコマンドでPrismaをインストールする。
cd backend
npm i prisma --save-dev
それに加えて、@prisma/client
もインストールしておく必要がある。これによって、TSでORMとしてPrismaを使うことができる。prisma generate
を実行することで、TS上で方安全なコードを記述することが可能になる。
もちろんDBとしてD1を使うので、@prisma/adapter-d1
もインストールしておく。
npm install @prisma/client @prisma/adapter-d1
次に以下のコマンドでPrismaの設定ファイルを作成する。
npx prisma init
これで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を参照してほしい。
まずはデータベースとなるD1を作成する。
npx wrangler d1 create __DATABASE_NAME__
__DATABASE_NAME__
には任意の名前を入れる。私はtutorial_d1
とした。すると、wrangler.json(もしくはwrangler.toml)に以下のような記述があるはずだ。
{
"d1_databases": [
{
"binding": "DB",
"database_name": "tutorial_d1",
"database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // ここにIDが入る
}
]
}
これでD1は作成されたので、prisma/schema.prisma
を元にマイグレーションファイルを作成する。
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
ディレクトリに作成される。中身を見ると以下のようになっている。
-- CreateTable
CREATE TABLE "todo" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"done" BOOLEAN NOT NULL
);
これを反映させればD1内にテーブルが作られる。以下のコマンドでマイグレーションファイルを適用する。
npx wrangler d1 migrations apply __DATABASE_NAME__ --local
これでローカルのD1内にテーブルが作成される。ちなみに、localをremoteに変えることで本番環境に適用できる。
ここでしっかり動作しているか確かめるために、Prisma studioを使ってみる。以下のコマンドでPrisma studioを起動する。
npx prisma studio
これで、http://localhost:5555にアクセスすると、以下のような画面が表示される。
Add Record
と書かれた部分を押せばデータ追加もできる。これでDBの作成は完了である。
APIの作成
次にAPIを作成していく。とはいっても全部で3ファイルほどしか作成してないので、一気に説明してしまう。
まずはsrc/
ディレクトリにBindings.ts
を作成し、以下のように記述する。
export type Bindings = {
DB: D1Database;
};
これはBindingsはwrangler.jsonで設定した値などを適用するためのものなのだが、その型ファイルである。
次に同じ階層に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
を以下のように修正する。
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の内容を書き換える仕組みは作っていない。フロント部分も合わせて追加で作成してみると理解が深まるかもしれない。
動作確認
それでは早速、ローカルにおいて動作確認を行なってみる。
バックエンドとフロントエンドを起動させる。
cd backend
npm run dev
cd ../frontend
npm run dev
これで、http://localhost:5173にアクセスすれば、Todoリストっぽいことができるようになっているはずだ。
デプロイ
ついにこの時が来た。デプロイを行って、他の人にも見せることができるようにする。
まずは Cloudflare workersとしてバックエンドをデプロイする。以下のコマンドでデプロイを行う。
cd backend
npm run build
この時、デプロイ先のURLが表示されるので、後ですぐに確認できるようにしておく。
次に、さっきローカルで作成したD1を今回はリモートに適用する。
npx wrangler d1 migrations apply __DATABASE_NAME__ --remote
これで、D1にテーブルが作成される。最後にフロントエンドだが、これは他と比べて少しだけ面倒である。まずこのアプリ自体をgitにpushしておく。そしてCloudflare Pagesのページに行って、git上のリポジトリを選択してビルド設定を行う。
設定は以下のように行えばよい。環境変数としてAPIのエンドポイントを設定しておけば、デプロイしたフロントエンドアプリがバックエンドのAPIを叩くことができる。
これでデプロイが勝手に行われる。しかも嬉しいことにGitと連携しているので、Git側で何か変更があれば自動で再デプロイしてくれる。
今回はやらないが、バックエンド側もCICDを使って自動デプロイを行うことができれば、さらに開発効率を上げることができるだろう。
数分待った後でフロントエンドのデプロイ先URLにアクセスすれば、ローカルで確認できた同じ動作が確認できるだろう。
おめでとう!!!
終わりに
こんなに手早く簡単に、かつ無料でフルスタックアプリを作れるというのは本当にありがたい。無料でも、これで出来ることの幅が大分広がったことは間違いないだろう。もっと拡張していけば、Auth0を使って認証機能を追加したSNSアプリや、予約システム、チャットアプリなど、様々なアイデアを形にできる。
そもそもどんなスキルにおいても、能力を伸ばす一番有効な手段がアウトプットであると思っている(人に教えるということもあり)。どんどんアウトプットして、少しずつ新しい技術を取り入れていけば、エンジニアとしてのスキルは確実に上がっていくし、何より楽しく開発ライフを送ることができる。かつそれがデプロイできれば、他の人へ分かりやすくスキルを伝えることができる。
ちなみに私は今回のような技術構成でポートフォリオを作成しているのだが、D1にマスタデータとして内容を保存しておけば、内容が更新されるたびにいちいち編集してデプロイし直す必要はない。D1の中身を変えるだけで反映される。ポートフォリオは更新頻度が高いので、とても便利だと思っている。
もしよかったら私のポートフォリオをぜひ見ていってほしい。
~ 最後まで読んでいただき、ありがとうございました ~