React + Hono + Azure Functions で作る Azure Static Web Apps入門
概要
みなさんは Azure サービスを使って開発したことがありますか?自分は AWS のサービスを使った経験はあったのですが、Azure サービスは使ったことがありませんでした。
仕事では Azure サービスも使っているため、個人でも勉強しようと思い簡単なアプリを Azure サービスを使ってデプロイしたので、こちらを紹介します。
デプロイする Azure サービスは Azure Static Web Apps を使います。Azure Static Web Apps は、フロントエンドからバックエンドまでをシームレスにデプロイできるサービスです。
そして Azure Static Web apps にデプロイするものとして、UI 側を React + Vite、API 側を Hono + Azure Functions を使っていきます。
前提
- Azure アカウント登録済み
今回のコードは以下です。
Azure Functions Core Tools のインストール
Azure Functions Core Tools はローカルで Azure Functions を開発できるツールです。
npm install -g azure-functions-core-tools@4 --unsafe-perm true
# インストールできたかバージョン確認
func -v
これで func コマンドが使えるようになります。次からこのコマンドで実際に Azure Functions のプロジェクトを作っていきましょう。
バックエンド (Hono + Azure Functions) の実装 ✅
Azure Functions をローカルに構築
Azure Functions は FaaS(Function as a Service)に分類される Azure サービスです。そのためイベントドリブン型のサーバーレスコードを提供します。
今回のイベントは Http をトリガーに実行してほしいので”Http Trigger”として Azure Functions を構築していきます。
以下のコマンドでサクッと構築しましょう。
# apiフォルダを作成
mkdir api
cd api
# Azure Functionsを作成
func new --template "Http Trigger" --name httpTrigger
# 選択肢がでてくるので今回はnode、typescriptを順に選ぶ
Use the up/down arrow keys to select a worker runtime:node
Use the up/down arrow keys to select a language:typescript
src/functions 配下に httpTrigger.ts が作成されたと思います。ここで Azure Functions のトリガーを決めることができます。
この httpTrigger.ts を下記の様に修正しましょう。Azure Functions の handler は後ほど Hono で作成するので今はこのままで大丈夫です。
import { app } from "@azure/functions";
import { azureHonoHandler } from "@marplex/hono-azurefunc-adapter";
import honoApp from "../index.js";
app.http("httpTrigger", {
methods: ["GET", "POST", "DELETE", "PUT"],
authLevel: "anonymous",
route: "{*proxy}",
handler: azureHonoHandler(honoApp.fetch),
});
authLevel には今回は簡単な Todo アプリなので誰でもアクセスできる”anonymous”を設定します。本来はここに認証を入れます。
route は”{*proxy}”とすることで、Hono 側で自由にルーティングを決めることができます。
ここまでで Azure Functions の土台はできました。
npm install -D typescript@latest
Hono を使用した API 実装
先ほど作った Azure Functions にのせる Hono をインストールしていきましょう。
Hono のインストール
ここでは Hono と Azure Functions アダプターをインストールします。
npm i @marplex/hono-azurefunc-adapter hono
index.ts に下記の様にルーティングを作りましょう。
import { Hono } from "hono";
import todo from "./todoRouter";
const app = new Hono();
app.get("/api", (c) => {
return c.text("Hello Hono!");
});
app.route("/api/todos", todo);
export default app;
次に/api/todos にルーティングされるエンドポイントを実装していきましょう。
今回の Todo アプリで最低限のバリデーションを作成しましょう。
zod と Hono が提供しているバリデーターをいれましょう。
npm i zod @hono/zod-validator
todoRouter は基本的な CRUD 処理にしています。
import { Hono } from "hono";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
import { prisma } from "./lib/prisma";
const todo = new Hono();
// バリデーションスキーマ
const todoSchema = z.object({
title: z.string().min(1).max(50),
});
// Todo一覧取得
todo.get("/", async (c) => {
const todos = await prisma.todo.findMany({
orderBy: { id: "desc" },
});
return c.json({ todos });
});
// Todo作成
todo.post("/", zValidator("json", todoSchema), async (c) => {
const { title } = c.req.valid("json");
await prisma.todo.create({
data: {
title,
isCompleted: false,
},
});
return c.json({ message: "Created" }, 201);
});
// Todo更新
todo.put("/:id", async (c) => {
const id = parseInt(c.req.param("id"));
const { isCompleted } = await c.req.json();
try {
const updatedTodo = await prisma.todo.update({
where: { id },
data: { isCompleted },
});
return c.json(updatedTodo);
} catch (error) {
return c.json({ message: "Todo not found" }, 404);
}
});
// Todo削除
todo.delete("/:id", async (c) => {
const id = parseInt(c.req.param("id"));
try {
await prisma.todo.delete({
where: { id },
});
return c.json({ message: "Todo deleted" });
} catch (error) {
return c.json({ message: "Todo not found" }, 404);
}
});
export default todo;
次に DB を作成していきましょう。ここでは Prisma + Supabase でサクッと接続しましょう。
Prisma の追加
# Prismaのインストール
npm i prisma @prisma/client
# Prismaの初期化
npx prisma init
Supabase のダッシュボードへいき、今回用の DB をつくりましょう。
作成出来たら ORMs へいき、Tool が Prisma になっていることを確認、コピーして.env ファイルに張り付けましょう。
次に実際の Todo モデルを schema.prisma に作成しましょう。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
model Todo {
id Int @id @default(autoincrement())
title String
createdAt DateTime @default(now())
isCompleted Boolean @default(false)
}
マイグレーションして、Supabase か prisma studio でテーブルが作成されていることを確認しましょう。
# マイグレーション
npx prisma migrate dev --name init
# Prisma studio
npx prisma studio
無事に作成されていれば OK です。
最後に Seed データを作りましょう。
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const todos = [
{
title: "Learn Prisma",
isCompleted: false,
},
{
title: "Build API with Hono",
isCompleted: true,
},
{
title: "Deploy to Azure",
isCompleted: false,
},
];
for (const todo of todos) {
await prisma.todo.create({
data: todo,
});
}
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
"start": "func start",
"test": "npx jest"
},
+ "prisma": {
+ "seed": "ts-node prisma/seed.ts"
+ },
"dependencies": {
"@azure/functions": "^4.0.0",
"@hono/zod-validator": "^0.4.2",
この seed コマンドをうちましょう。
# seedデータ追加
npx prisma db seed
npm i -D ts-node
ここまででバックエンドの実装が終わりました。実際にローカルで Azure Functions を動かしてみましょう。
# build
npm run build
# Azure Functions起動
npm run start
以下が出てきたら起動に成功しています。
curl で叩いてレスポンスが返ってくるか確認。
curl http://localhost:7071/api/todos
または
curl http://localhost:7071/api
フロントエンドの実装 ✅
フロントは React + Vite で構築し、スタイルは Tailwindcss を使っていきます。
# apiディレクトリから移動rootに移動
cd ../
# clientというapp名で構築
npm create vite@latest client -- --template react-ts
# clientディレクトリに移動
cd client
# パッケージインストール
npm i
Tailwindcss の追加
公式サイトをみてサクッと追加しましょう。
npm install tailwindcss @tailwindcss/vite
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});
@import "tailwindcss";
たったこれだけで Tailwindcss が使えるようになります。
コンポーネント構造
それでは React のコンポーネントを作っていきましょう。
簡単な Todo アプリなので大きく 2 つのタスクを追加するフォームとタスク一覧のコンポーネントを用意します。
import { useTodos } from "../hooks/useTodos";
export const AddTodoForm = () => {
const { title, setTitle, addTodo } = useTodos();
return (
<div className="flex gap-2 justify-center">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="新しいタスク"
className="border p-2 rounded"
/>
<button
onClick={addTodo}
className="bg-blue-500 text-white px-4 py-2 rounded cursor-pointer"
>
追加
</button>
</div>
);
};
import { useTodos } from "../hooks/useTodos";
import { TodoItem } from "./TodoItem";
export const TodoList = () => {
const { todos, toggleTodo, deleteTodo } = useTodos();
return (
<ul className="space-y-4 mt-5">
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
);
};
TodoList コンポーネントはもう少し、TodoItem としてコンポーネント分割しましょう。
import { Todo } from "../types/todo";
interface TodoItemProps {
todo: Todo;
onToggle: (id: string, isCompleted: boolean) => void;
onDelete: (id: string) => void;
}
export const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => {
return (
<li className="flex items-center justify-between p-4 bg-white rounded-lg shadow">
<div className="flex items-center space-x-4">
<input
type="checkbox"
checked={todo.isCompleted}
className="h-4 w-4 text-blue-600 rounded"
onChange={() => onToggle(todo.id, todo.isCompleted)}
/>
<span className={todo.isCompleted ? "line-through text-gray-400" : ""}>
{todo.title}
</span>
</div>
<button
onClick={() => onDelete(todo.id)}
className="px-3 py-1 text-red-500 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
>
削除
</button>
</li>
);
};
export interface Todo {
id: string;
title: string;
createdAt: Date;
isCompleted: boolean;
}
API との連携
先ほど作成した Azure Functions に乗せた Hono の API を呼べるように連携しましょう。
ここでは fetch するメソッドをまとめています。Azure Functions を統合している場合、パスより前はプロキシが動いてくれるので fetch するエンドポイントの指定も/api 以降で問題ありません。
const BASE_URL = "/api/todos";
const HEADERS = {
"Content-Type": "application/json",
};
export const todoApi = {
fetchTodos: async () => {
const res = await fetch(BASE_URL);
const data = await res.json();
return data.todos;
},
addTodo: async (title: string) => {
await fetch(BASE_URL, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ title }),
});
},
updateTodo: async (id: string, isCompleted: boolean) => {
await fetch(`${BASE_URL}/${id}`, {
method: "PUT",
headers: HEADERS,
body: JSON.stringify({ isCompleted }),
});
},
deleteTodo: async (id: string) => {
await fetch(`${BASE_URL}/${id}`, {
method: "DELETE",
});
},
};
useTodos という hooks を作って、コンポーネント側で楽に使えるようにしましょう。
import { useEffect, useState } from "react";
import { todoApi } from "../api/todoApi";
import { Todo } from "../types/todo";
export const useTodos = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [title, setTitle] = useState("");
const fetchTodos = async () => {
const data = await todoApi.fetchTodos();
setTodos(data);
};
const addTodo = async () => {
if (!title.trim()) return;
await todoApi.addTodo(title);
setTitle("");
};
const toggleTodo = async (id: string, isCompleted: boolean) => {
await todoApi.updateTodo(id, !isCompleted);
setTodos(
todos.map((t) =>
t.id === id ? { ...t, isCompleted: !t.isCompleted } : t
)
);
};
const deleteTodo = async (id: string) => {
const isConfirmed = window.confirm("本当に削除しますか?");
if (!isConfirmed) return;
await todoApi.deleteTodo(id);
setTodos(todos.filter((t) => t.id !== id));
};
useEffect(() => {
fetchTodos();
}, []);
return {
todos,
title,
setTitle,
addTodo,
toggleTodo,
deleteTodo,
};
};
ここまででフロントエンドは概ね実装し終わりました。早速ローカル環境で確認したいところですがここで便利な CLI ツールを入れておきましょう。
SWA CLI の設定
Static Web Apps の CLI です。ビルドやデプロイが簡単にできるので入れておきましょう。
npm install -g @azure/static-web-apps-cli
# swaの設定ファイルを作成
swa init
設定ファイルは自動生成してくれます
{
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
"configurations": {
"swa-cli": {
"appLocation": "client",
"apiLocation": "api",
"outputLocation": "dist",
"apiLanguage": "node",
"apiVersion": "16",
"appBuildCommand": "npm run build",
"apiBuildCommand": "npm run build --if-present",
"run": "npm run dev",
"appDevserverUrl": "http://localhost:5173"
}
}
}
ここまでできたらローカル環境で Static Web Apps がうまく動くかブラウザで確認してみましょう!
先ほど自動生成した swa の設定ファイルをもとに build し、Azure Functions を統合させた Azure Static Web Apps の環境をエミュレートしてくれます。
# client、api両方を一緒にbuildする
swa build
# ローカルでStatic Web Appをエミュレートする
swa start
localhost:4280 へアクセス
お疲れ様でした。ローカル環境での開発は以上です。ここからはいよいよ Azure Static Web Apps へデプロイしていきましょう。
デプロイ手順 ✅
Azure Portal へログイン
Static Web Apps を作成
リソースグループから「+追加」押下。
static web apps と検索。
名前は今回は「react-todo」にしています。
ホスティングプランは Free にします。
GitHub リポジトリとの連携
初めての場合は GitHub アカウントの認証をすればリポジトリを選べるようになります。
リポジトリを選択するとビルドのプリセットを検出してくれ、アプリの場所と API の場所、出力先を自動的に入力してくれます。
アプリの場所は React のアプリが入っているディレクトリを、API は Hono、Azure Functions が入っているディレクトリになっていることを確認しましょう
出力先は Build した時にできる成果物を指定するようにしましょう。特に設定をしていない場合は dist フォルダ内に作られるのでdistに変更しておきましょう
そこまでできたら、次のデプロイ構成にいきましょう
デプロイ構成ではデプロイトークンを使用しましょう
詳細設計とタグは特に変更しなくても問題ないです
では「確認および作成」で作成しましょう!
デプロイ中が完了すると以下の表示になるので、次の手順のところの「リソースに移動」をクリックしましょう
以上でデプロイまで完了しました。デプロイは GitHub Actions で行うので今後の開発はリポジトリへ push したら CI/CD が走ります。
また、Free プランだと最大 3 つまで Staging 環境が使えます。ブランチを切って PR を作れば自動的に Staging 環境になって挙動を確認できます。
まとめ
今回は React、Hono(Azure Functions)をローカル環境で開発し、Azure Static Web Apps にデプロイする一連の流れを紹介しました。今回の Azure サービスは個人の趣味程度なら無料で使える範囲内なので、気になったら気軽に使っていきましょう!
参考
Discussion