Co-Edo終了報告サイトをCloudflareで開発するとき使った NuxtHub がとっても便利!
こんにちは。田中弘治です。
僕は2013年よりコワーキングスペース茅場町 Co-Edoというのを運営してきているのですが、実はまことに残念なことに、ビル建て替えに伴い本年中に終了することとなりました。
これまでご活用いただいたみなさま、本当にありがとうございます。
その報告をする専用サイトを作ろうと思い、どうせならCo-Edoへのメッセージを送れるようなものをと考えてたところで Cloudflare を活用しようと思いました。
D1 や KV といったエッジで動く技術は今後広まっていくと思うので、今後 Cloudflare 中心の開発をしていくための準備でもあります。
作ったもの
1週間も2週間もかけてつくるものではないので、あっさりしたものを作りました。
作っていくうちに、いろいろやりたくなり、結果的につぎのような機能を持つサイトになりました。
- メッセージを送る機能
- 今後の最新情報を受け取るためのメール登録
- コラボレーター募集
- 寄付募集
いろいろ試しながら作りましたが、数日程度で開発しています。
(といっても管理画面側は公開後につくってます)
技術スタック
基本的にはいつもの Nuxt 3 を中心とした構成です。
- TypeScript
- Nuxt3
- Cloudflare Pages (Hosting & functions)
- Cloudflare D1 (SQLite)
- Cloudflare KV (Key-Value Store)
- Resend (メール送信)
- Stripe (決済)
- Sentry
- その他 Nuxt・Vue.js 関連
- NuxtHub (後述)
- Nuxt UI Pro (UI フレームワーク)
- Nuxt Auth Utils (認証・これから利用)
- VueEmail (メール送信用のツール・コンポーネント)
- その他
(太字は今回はじめて使ったもの。Sentry は業務委託での使用を除く)
NuxtHub について
NuxtHub は Cloudflare での開発をサポートするツール・管理画面などをまとめて提供するサービスです。
(NuxtLabs が提供しています)
つぎのような特徴があります。
- Cloudflare の各種サービスを使ったサイトを効率的に開発できる
- Cloudflare Pages や D1, KV などの設定・データの確認を管理画面で行える
- Devtools で開発環境時のデバッグにも対応
- D1, KV, R2 などを扱うコンポーザブルや Hooks などを提供
- リモートの DB マイグレーション等を行う CLI 機能を提供
たとえば Vercel や Netlify を活用しているかたは、その管理画面などのUIを Cloudflare で活用できるとイメージするとよいかもしれません。
なお、記事執筆時点ではベータ版です。
(現時点では D1, KV, R2, AI および サーバー側の Cache storage のみサポートされています。今後は認証やメール関連のサービスが追加される予定)
料金は?
Cloudflare の各種技術はほぼ無料やそれに近い低額で利用できます。
(オンボーディング中に従量制のプランにすることを勧める文章はありました。それでも上述のようにほぼ無料で使えるでしょう)
NuxtHub も無料プランもありますが、基本的に有料のツールです。
有料でも月10ドル。プロジェクト数など無制限に使えるので、 Vercel などと比べるとかなりお得だと思います。
僕も無料枠の5プロジェクトに到達したら、すぐ有料にする予定です。
Cloudflare スタックについて
CDNのエッジで動くデータベースをはじめとする Cloudflare が提供する開発者向けの各種サービス群は、とても魅力的です。
これまでの Web 開発の常識を覆すようなものが多いので、これからの Web 開発には欠かせない存在になっていくかもしれません。
たとえば下記の記事のように、Cloudflare スタックでのサービス開発の話も増えてきています。
(たくさん記事を書いている Cloudflare の方の記事はとても勉強になります)
一方で Cloudflare 特有の制限などもあるため、まだまだ知見をためていく必要もあるでしょう。
(この手のサービスの無料枠を超える分は総じて負荷の大きい振る舞いといえるので、それらを回避・低減する開発手法の確立・定着が望まれているかと思います)
また Nuxt や Remix のようなメタフレームワークを使用せずとも unjs/nitro や Hono といった軽量かつ環境に依存しないフレームワークの相性もよいはずです。
これからより広範囲に活用されていくのは間違いないと思います。
NuxtHub ではじめる Cloudflare による Web アプリケーション開発の始め方
個人的には Nuxt スタックとも呼べるほど Nuxt による開発体験は気に入っているのですが、この NuxtHub はその象徴ともいえるものです。
まだ NuxtHub はベータ版ということもあり、試してないという方も多いと思いますので、この記事ではその概要を紹介したいと思います。
Co-Edoの終了告知サイトにおいて、例えばメッセージ投稿機能では、フォーム送信→データベース保存→メール送信を行うといった一連の処理をサーバーサイドのAPIにしています。
Nuxt に慣れていれば /api/foo
などでサーバー側の記述ができることをご存知かと思いますが、そのなかで NuxtHub の用意する hubDatabase()
等のコンポーザブルを使用して Cloudflare D1 へのデータ保存というのが基本的な流れです。
NuxtHub で最初のプロジェクトを作成する
NuxtHub のサイト右上の Sign up で GitHub アカウントを使用し NuxtHub のアカウントを作成します。
GitHub アカウントでログインするように求められます。
GitHub の認証画面で許可を求められます。
NuxtHub の管理画面に移ります。
まずは、Cloudflare の API トークンを設定します。
まだ Cloudflare のアカウントがない方は、まずはアカウントを作成しましょう。
長いので折りたたみます
開発者プラットフォームから Cloudflare Pages を選択します。
無料プランで開始します。
メールが届くので、メール内のリンクをクリックして Verify します。
Workers and Pages を開きます。
日本語に変更できます。
NuxtHub の管理画面に戻り
Create a token with required permissions をクリックします。
Account Resources を選択します(All Accounts でもOKです)
作成する内容を確認し Create Token をクリックします。
API Token をコピーし、NuxtHub の画面に貼り付けます。
(注意書きのとおり、再表示はできませんので、失くした場合は再作成・再設定となります)
すべて終わると、自動的に確認が走り、NuxtHub と Cloudflare の連携が完了します。
(Freeプランでも利用可能ですが、1MBのバンドルサイズの制限もあるので、従量制のプランに変更しておくことが強く推奨されています)
プロジェクトを作成する
あとはプロジェクトを作成するだけです。
テンプレートから選択するとすぐにデプロイまでできるため、どのように開発するか理解できます。
(NuxtHub Starter がおすすめです)
お疲れ様でした。
NuxtHub の Slug はアカウントページで変更可能です。
NuxtHub による開発
インストール
いちからはじめるのであれば下記コマンドでプロジェクトを作成するだけです。
npx nuxthub init my-app
もし、既存のプロジェクトに NuxtHub を導入したい場合は、下記のようにします。
npx nuxi@latest module add hub
nuxt.config.ts
設定 使うサービスを設定します。
export default defineNuxtConfig({
modules: ['@nuxthub/core'],
hub: {
database: true,
kv: true,
blob: true,
cache: true,
},
})
Starter Template を参考にするとよいでしょう。
(実際の挙動も確認できるのでこれをデプロイしておくと便利です)
環境変数を設定
Cloudflare Pages のビルド時やruntimeで使用する環境変数は、開発環境では .env
ファイルに記述しておき、本番環境は NuxtHub の管理画面から設定します。
(.env の内容を一括してコピー&ペーストで設定可能なので便利です)
NUXT_FOO_BAR=foobar
なお(Stripe の API キーのような)機密性の高い情報は暗号化して保存することができます。
管理画面からは値入力欄の右側に鍵のアイコンをクリックして設定します(以後値の確認はできません)
デプロイ
指定した Production branch (デプロイ対象のブランチ)が GitHub 上で更新されると自動でデプロイプロセスが開始されます。
(NuxtHub の管理画面でデプロイ状況・デプロイ結果を確認できます。デプロイに要する時間も比較的短いです)
データベースの操作
Cloudflare D1 は SQLite をベースにしたエッジで動くデータベースです。
KV がシンプルな key-value 型のデータを扱うのに対して、D1 は SQL を使って複雑な構造のデータを扱うことができます。
NuxtHub では Drizzle ORM と連携した操作が可能です。
(SQLで扱うことも可能)
(もう何百年もSQLを書いていないので)今回は Drizzle ORM を使います。
Drizzle ORM
基本的な流れはつぎのようになります。
- 事前準備:
drizzle.config.ts
で設定 -
server/database/schema.ts
でスキーマを定義 -
npm run db:generate
でマイグレーションファイルを生成 -
npm run dev
によりマイグレーションが実行される
以後、テーブルのschema変更等は 1~3 を行います。
Drizzle ORM のインストール手順
npm install drizzle-orm drizzle-kit
必要な準備をします。
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'sqlite',
schema: './server/database/schema.ts',
out: './server/database/migrations',
})
"scripts": {
"db:generate": "drizzle-kit generate", // ←追加
}
import { consola } from 'consola'
import { migrate } from 'drizzle-orm/d1/migrator'
export default defineNitroPlugin(async () => {
if (!import.meta.dev) return
onHubReady(async () => {
// 開発サーバー起動時にマイグレーションを実行
await migrate(useDrizzle(), { migrationsFolder: 'server/database/migrations' })
.then(() => {
consola.success('Database migrations done')
})
.catch((err) => {
consola.error('Database migrations failed', err)
})
})
})
server/database/schema.ts
でスキーマを定義します。
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
// 必要な数だけ記述
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
password: text('password').notNull(),
avatar: text('avatar').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
})
server/utils/drizzle.ts
で Drizzle を扱う関数と型情報を定義します。
import { drizzle } from 'drizzle-orm/d1'
export { sql, eq, and, or } from 'drizzle-orm'
import * as schema from '../database/schema'
export const tables = schema
export function useDrizzle() {
return drizzle(hubDatabase(), { schema })
}
// テーブルを増やすたびに追記
export type User = typeof schema.users.$inferInsert
データの取得
export default eventHandler(async () => {
const users = await useDrizzle().select().from(tables.users).all()
return users
})
とても簡潔に書けますね。
もちろん呼び出し側では、型情報が付与されたデータを受け取ることができます。
データの挿入
バリデーション用に Valibot を使用します。
(詳細はフォーム作成にて)
下記は適当なサンプル実装です。
import * as v from 'valibot'
import { hashSync } from 'bcrypt'
import type { User } from '@@/server/utils/drizzle'
import type { UserSchema } from '@/composables/user'
export default eventHandler(async (event) => {
const parsedData = await readValidatedBody(event, v.parser(userSchema))
const user = await useDrizzle()
.insert(tables.users).values({
...parsedData,
password: hashSync(parsedData.password, 10),
createdAt: new Date(),
})
.returning()
.get()
.catch((error) => {
throw createError({
statusCode: 500,
statusContact: 'Failed to insert',
error,
})
})
return user
})
ほかの例は割愛します。
メール送信
今後 NuxtHub ではメール送信機能も提供される予定です。
今回は Resend を使用しました。
あらかじめ Resend に登録し、API キーを取得しておきます。
(NuxtHub の管理画面から Cloudflare の環境変数を Secret で設定しておきます)
import * as v from 'valibot'
import { sendEmail } from '@@/server/utils/resend'
import type { UserContact } from '@@/server/utils/drizzle'
import { contactSchema } from '@/composables/contact'
export default defineEventHandler(async (event) => {
try {
const parsedData = await readValidatedBody(event, v.parser(contactSchema))
// データベースに保存する
const insertedData: UserContact = await useDrizzle().insert(tables.contacts).values({
...parsedData,
createdAt: new Date(),
}).returning().get()
// メールを送信する
const subject = `登録ありがとうございます(${insertedData.name}さま)`
const headers = {
'X-Entity-Ref-ID': `${insertedData.id}`,
'X-Entity-Ref-Time': `${insertedData.createdAt}`,
}
await sendEmail({
from: 'no-reply@coworking.tokyo.jp',
to: insertedData.email,
subject,
text: buildMail(insertedData),
headers,
})
return {
success: true,
error: null,
}
} catch (error) {
const message = error instanceof Error ? error.message : ''
throw createError({
statusCode: 400,
statusContact: `post message error: ${message}`,
})
}
})
const buildMail = (data: UserContact) => `
${data.name} さま
以下の内容でメッセージをお預かりしました。
-------------------------------------
■ 氏名: ${data.name}
■ メールアドレス: ${data.email}
-------------------------------------
--
コワーキングスペース茅場町 Co-Edo
`.trim()
import { Resend } from 'resend'
import type { CreateEmailOptions } from 'resend'
let resend: Resend
const useResendClient = () => {
if (!resend) {
resend = new Resend(useRuntimeConfig().resendKey)
}
return resend
}
export const sendEmail = async (options: CreateEmailOptions) => {
const resend = useResendClient()
const results = await resend.emails.send({
...options,
}).catch((error) => {
throw new Error(error)
})
// 送信終了
return results
}
VueEmail を使用する
サーバー側でも Vue のコンポーネントを使用したいところです。
調べたところ、いちから書くと下記のようになります。
サーバーサイドで Vue コンポーネントを使用する
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { createViteServer } from 'vite'
import vue from '@vitejs/plugin-vue'
let vite
export default defineEventHandler(async (event) => {
if (!vite) {
vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
plugins: [vue()],
})
}
const { default: MyComponent } = await vite.ssrLoadModule('/path/to/MyComponent.vue')
const app = createSSRApp(MyComponent, {
// コンポーネントに渡す props
prop1: 'value1',
prop2: 'value2',
})
const html = await renderToString(app)
// ここでhtmlを使ってメールを送信するなどの処理を行う
return html
})
となるのですが VueEmail がほぼ同じように実装していたので、HTMLメールで使用できるコンポーネントも用意されていたこともあり活用しました。
そんなわけで、下記のようにメール送信が可能です。
# 該当箇所のみ
// メールを送信する
const subject = `[Co-Edo]: ご登録ありがとうございます(${insertedData.email})`
const html = await render(RegisterMail,{
title: subject,
data: insertedData,
})
const results = await sendEmail({
from: 'no-reply@coworking.tokyo.jp',
bcc: 'info@coworking.tokyo.jp',
reply_to: insertedData.email,
to: insertedData.email,
subject,
html,
})
フォームとバリデーション
全体的な UI は Nuxt UI (Pro) を使用しました。
Nuxt UI の記事はこちらをどうぞ。
基本的には任意のコンポーネントに対し :ui
props で適用したいスタイルを Tailwind 記法で渡します。
<script setup lang="ts">
// 初期値はドキュメントで確認できる
const ui = {
title: 'text-lg font-bold',
description: 'mt-1 text-lg',
}
</script>
<template>
<UAlert
title="アラートはこんな感じ"
color="primary"
:ui
/>
</template>
メールフォームは <UForm>
でバリデーション等を扱うことができます。
Yup
, Zod
, Joi
, Valibot
などを扱いやすく作られています。
今回は Valibot を使用しました。
<script setup lang="ts">
import * as v from 'valibot'
import type { FormSubmitEvent } from '#ui/types'
const schema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'Must be at least 8 characters'))
})
type Schema = v.InferOutput<typeof schema>
const state = reactive({
email: '',
password: ''
})
const submit = async (event: FormSubmitEvent<Schema>) => {
const result = await $fetch('/api/login', {
method: 'POST',
body: event.data,
}).catch((error) => {
console.error(error)
})
if (result) {
await navigateTo('/dashboard')
}
}
</script>
<template>
<UForm
:schema="v.safeParser(schema)"
:state
class="space-y-4"
@submit="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>
<UButton type="submit">
Submit
</UButton>
</UForm>
</template>
NuxtHub 管理画面
最後に NuxtHub の管理画面について簡単に紹介します。
Cloudflare Pages は GitHub への push をトリガーに自動でデプロイを行いますが、NuxtHub はそのデプロイの状況を確認することもできます。
Database の操作や、管理コンソールもあります。
ちなみにこの UI は Nuxt UI の Dashboard で作られています。
有料版のユーザーは利用できますので、アプリケーションの管理画面はこれでつくると工数が削減できますね。
(まさにいま少しずつ作っているところです)
最後に
NuxtHub は Cloudflare のサービスを使った開発をサポートするツール・サービスです。
使ってみるとわかりますが、BaaS を使っているかのような感覚で、とても便利に開発可能です。
Cloudflare を使った開発に興味のあるフロントエンド開発者は、ぜひ試してみてください。
おまけ
今回さくっと数日で作ったのが、Co-Edoの終了告知サイトです。
Stripe を使った寄付もできるようになっています。
ぜひ試してみてください 😎
(あとバグ報告がありましたら、ぜひともコメントやメッセージでお知らせくださいませ 🙇♂️)
Discussion