コンテンツにスキップ

現在位置を記録するWebアプリを作って、Cloudflare D1の使い方を学ぶ

2026/04/22
(2026/04/28 更新)

Webアプリ開発にデータベースは欠かせませんので、その練習としてLocation Logger という現在位置を記録するWebアプリを作ってみました。 サーバーレス環境のCloudflare WorkersCloudflare D1を使っています。 ソースコードはGitHubに公開してあります。

アプリ開発を通じて学びたかったこと

Section titled “アプリ開発を通じて学びたかったこと”

Location Loggerの仕組みは以下のとおりです。

flowchart LR
  ui[**Vueアプリ**
     地図・地名表示、
     現在位置の取得]
  api[**Workers**
      Vueアプリの配信、
      REST API]
  db[**D1**
     位置情報の記録]

  subgraph client [Webブラウザ]
    direction LR
    ui
  end

  subgraph server [Cloudflare]
    direction LR
    api <--> db
  end

  ui <--> api

Vueを使ったSPA(シングルページアプリケーション)の開発とCloudflare Workersを使ったデプロイのやり方は習得済みでしたので、今回の開発で学びたかったことは以下になります。

  • Cloudflare Workersを使って、サーバーレスJavaScriptにREST APIを実装する方法を習得する
  • Cloudflare D1上にデータベースを作成し、それにアクセスするJavaScriptの実装方法を習得する

C3 CLIを使ってアプリ開発を開始する

Section titled “C3 CLIを使ってアプリ開発を開始する”

Cloudflareが提供しているC3 CLIを使って、Cloudflare D1を使ったWorkersアプリのプロジェクトを作成します。

Terminal window
npm create cloudflare@latest -- location-logger-example

質問事項(Yes / Noなど)はEnterキーで続行し、選択肢は以下の項目を選びます。

  • What would you like to start with? → Framework Starter
  • Which development framework do you want to use? → Vue
  • Would you like to use TypeScript? → TypeScript

プロジェクトのサブフォルダー構成は以下のようになっています。

location-logger-example
├── node_modules # npmで導入したパッケージが格納される
├── public # favicon.icoなどのファイルを格納する
├── server # REST APIの実装を格納する
└── src # Vueアプリの実装を格納する

まずは、Vueアプリ側で使用するパッケージをインストールします。

Vueアプリは数年前にLaravel上で作ったものを流用しているので、使用しているパッケージもそのときのままで少し古いです。

Terminal window
# 不要なパッケージのアンインストール
npm uninstall vue-router
# パッケージのインストール
npm i bootstrap axios leaflet geographiclib-geodesic nosleep.js \
datatables.net-dt datatables.net-select datatables.net-vue3

次に、WorkersとD1で使うパッケージをインストールします。HonoはCloudflare WorkersをサポートしているWebフレームワークです。また、drizzleはCloudflare D1をサポートしているORMです。

Terminal window
# Honoのインストール
npm i hono
# drizzleのインストール
npm i drizzle-orm
npm i -D drizzle-kit

本アプリでは、REST APIの実装にHonoを、データベースにアクセスするORMにdrizzleを使用しています。本アプリの規模ではこれらを使う必要はありませんが、 これらのパッケージの使い方も習得しておきたいと考えました。

Location Loggerの位置情報を記録するためのデータベースをCloudflare D1に作成します。

CloudflareのダッシュボードからGUIを使ってデータベースを作ることもできますが、今回はWranlgerコマンドを使います。初めて使用する際はCloudflareへのログインが必要です。

Terminal window
npx wrangler d1 create location_logger_example_db --binding DB --use-remote

CloudflareのダッシュボードでD1 SQL データベースを表示すると、location_logger_example_dbという名前のデータベースが作られていることを確認できます。

また、wrangler.jsoncを見ると、以下のようにd1_databasesのバインディングが追加されます。

wrangler.jsonc
"d1_databases": [
{
"binding": "DB",
"database_name": "location_logger_example_db",
"database_id": "<D1データベースのdatabaseId>",
"remote": true
}
]

"remote": trueを設定すると、npm run devでローカル起動した場合もD1データベースにアクセスするようになります。 デバッグ時はローカルにDBを作ることもできますが、今回はD1データベースの練習も兼ねて、ローカルにDBを作らずに進めます。

サーバーレスのJavaScriptからd1_databasesのバインディングを利用できるように型を生成します。

Terminal window
npx wrangler types

型がworker-configuration.d.tsに追加されます。

worker-configuration.d.ts
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./server/index");
}
interface Env {
DB: D1Database;
}
}

drizzle経由でD1データベースにアクセスするため、./drizzle.config.tsファイルを作成します。

drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './drizzle/schema.ts',
out: './drizzle/migrations',
dialect: 'sqlite',
driver: 'd1-http',
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
token: process.env.CLOUDFLARE_D1_TOKEN!,
},
})

dbCredentialsには環境変数を使っています。.envで環境変数の値を設定します。

  • CLOUDFLARE_ACCOUNT_IDは、Cloudflareのダッシュボードで確認できます。
  • CLOUDFLARE_D1_TOKENは、Cloudflareのダッシュボードからプロフィールを開き、サイドバーのAPIトークンを選択してトークンを作成するボタンを押して、カスタムトークンを作成してください。
.env
CLOUDFLARE_ACCOUNT_ID=<Workers & PagesのAccount Detailsに記載されているAccount ID>
CLOUDFLARE_DATABASE_ID=<D1データベースのdatabaseId>
CLOUDFLARE_D1_TOKEN=<D1の編集権を備えたカスタムAPIトークン>

./drizzle/schema.tsを作成して、D1データベースに作成するテーブルのモデルを実装します。

schema.ts
import { sql } from 'drizzle-orm'
import { sqliteTable, int, text, real } from 'drizzle-orm/sqlite-core'
export const clients = sqliteTable('clients', {
id: int('id').primaryKey({ autoIncrement: true }),
cid: text('cid').notNull().unique(),
ua: text('ua').notNull(),
createdAt: text('created_at')
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
})
export const locations = sqliteTable('locations', {
id: int('id').primaryKey({ autoIncrement: true }),
timestamp: int('timestamp').notNull(),
latitude: real('latitude').notNull(),
longitude: real('longitude').notNull(),
distance: real('distance'),
address: text('address').notNull(),
clientId: int('client_id').references(() => clients.id),
createdAt: text('created_at')
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
})

./drizzle/schema.tsで定義したテーブルのマイグレーションファイルを生成します。マイグレーションファイルの保存先は./drizzle.config.tsoutで設定した./drizzle/migrationsです。

Terminal window
npx drizzle-kit generate

マイグレーションファイルが正常に生成できたら、D1データベースにマイグレーションを実行します。

Terminal window
npx drizzle-kit migrate

./drizzle/schema.tsを変更する度に、マイグレーションファイルの生成と実行を都度行います。

テーブルの確認はCloudflareのダッシュボードからも確認できますが、テーブル数だけ表示されます。D1データベース内のテーブルの一覧を得るためにSQLを実行します。

Terminal window
npx wrangler d1 execute location_logger_example_db \
--remote --command \
"SELECT tbl_name, sql FROM sqlite_schema WHERE type ='table';"

Location LoggerのREST APIを./server/index.tsに実装します。以下は一部省略しています。詳細はGitHubのindex.tsでご覧いただけます。

index.ts
import { Hono, Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
import { getCookie, setCookie } from 'hono/cookie'
import { drizzle, DrizzleD1Database } from 'drizzle-orm/d1'
import { clients, locations } from '../drizzle/schema'
import { Client, Location } from '../src/shared/model'
import { eq, desc } from 'drizzle-orm'
const VERSION = '0.0.1'
const MAX_LOCATION_COUNT = 300
type ClientRow = typeof clients.$inferSelect
type LocationRow = typeof locations.$inferSelect
const app = new Hono<{ Bindings: Env }>()
app.onError((err: Error | HTTPException, c: Context) => {
/* エラーハンドリング: 省略 */
})
app.get('/api/v1/misc/version', async (c: Context) => {
return c.json({ version: VERSION })
})
app.get('/api/v1/client', async (c: Context) => {
const db = drizzle(c.env.DB)
const client = await getClient(db, c)
return c.json({ cid: client.cid })
})
app.get('/api/v1/location', async (c: Context) => {
const db = drizzle(c.env.DB)
const client = await getClient(db, c)
const locationRows: LocationRow[] = await db.select() /* 位置情報の一覧取得: 省略 */
return c.json(
/* 中略 */
)
})
app.post('/api/v1/location', async (c: Context) => {
const db = drizzle(c.env.DB)
const client = await getClient(db, c)
const location = await c.req.json<Location>()
const locationRow = await db
.insert(locations)
/* 省略 */
.returning()
return c.json({ id: locationRow[0].id })
})
async function getClient(
db: DrizzleD1Database<Record<string, never>>,
c: Context
): Promise<Client> {
/* 省略 */
}
export default app

./src/shared/model.d.tsというファイルを作成して、REST APIの型を定義しています。

model.d.ts
export interface Client {
id: number
cid: string
ua: string
createdAt: string
}
export interface Location {
timestamp: number
latitude: number
longitude: number
distance: number | null
address: string
clientId: number
createdAt: string
}

Vueアプリは./index.html./srcフォルダーの中身で実装します。 以下は、数年前に作ったVueアプリのコンポーネントを移植したときの手順です。本記事の主旨ではありませんので、省略します。

Vueアプリの移植作業の手順

./index.htmlは、<title>だけ変更し、他はデフォルトのままです。

./src/main.tsは、app.use(router)などのVue Routerに関する行を削除して、import 'bootstrap'を追加します。これに伴い、./src/views./src/routerは削除します。

./src/App.vueには、作成済みのVueアプリのコンポーネントを<template>の中に組み込みます。./src/components配下のファイル・フォルダーは全て削除して、作成済みのVueアプリのコンポーネントをコピーします。 REST APIのエンドポイントが少し変わったので、API呼び出し周りを修正しています。

./src/assets/base.cssmain.cssだけを残しておき、main.cssの中を数年前に作ったCSSで置き換えます。

なお、数年前に作ったVueアプリのコンポーネントはTypeScriptを使っていません。このため、TypeScriptで実装されているApp.vueでコンポーネントの型が解決できないとエラーになります。

そのため、./src/shims.d.tsにJavaScript Vueコンポーネントの型を定義しています。

shims.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

npm run buildでビルドエラーが発生しないか確認したところ、以下のメッセージが表示されます。

(!) Some chunks are larger than 500 kB after minification. Consider:
...

ビルドしたVueアプリが500KBを超えてしまうため、/.vite.config.tsに以下の設定を追加します。

vite.config.ts
export default defineConfig({
build: {
chunkSizeWarningLimit: 1000,
},
plugins: [
// 以下略

これで再度ビルドを実行すると、エラーが無くなりました。

ローカル環境でLocation Loggerが動くことを確認します。

Terminal window
npm run dev

右側にCID: xxxxxxxxと表示されている部分は、Cookieに設定しているUUIDです。このUUIDはD1データベースのclientsテーブルに登録されていることを示しています。SQLでclientsテーブルの内容を確認したところ、画面に表示されているUUIDはclientsテーブルに登録されていました。

Terminal window
npx wrangler d1 execute location_logger_example_db \
--remote --command "SELECT * FROM clients;"

REST APIをデバッグするために、./.vscode/launch.jsonを作成します。

launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "devサーバにアタッチする",
"type": "node",
"request": "attach",
"port": 9229
}
]
}

これでnpm run devで動かしている間にデバッガをアタッチできるようになります。

GitHub経由でのデプロイ方法は、以前の記事を参考にしてただければと思います。今回はC3 CLIを使ってCloudflare Workersにデプロイします。

Terminal window
npm run deploy

ターミナルには以下のようなメッセージが表示され、問題なくデプロイできました。(***はアカウント名)

Uploaded location-logger-example (8.07 sec)
Deployed location-logger-example triggers (1.05 sec)
https://location-logger-example.***.workers.dev
Current Version ID: adc9e8c4-64c2-4645-a14a-a982d7af163a

Cloudflareのダッシュボードを見ると、デプロイしたことを確認できます。

workers.devのURLを開くと、Cloudflare Workersが配信するLocation Loggerの画面が開いて、現在位置を取得・記録することができます。Workers上でREST APIが正常に動作しているかどうかは、以下のコマンドでも確認できます。

Terminal window
curl https://location-logger-example.***.workers.dev/api/v1/misc/version

{"version":"0.0.1"}が返ってくれば正常に動作しています。

今回学ぶことが出来たのは、下図の斜体文字の部分になります。

flowchart LR
  ui["`**Vueアプリ**`"]
  api["`**Workers**
      REST API: _Hono_
      ORM: _Drizzle ORM_`"]
  db["`**D1**
     DB操作: _Wrangler_
     マイグレーション: _Drizzle Kit_`"]

  subgraph client [Webブラウザ]
    direction LR
    ui
  end

  subgraph server [Cloudflare]
    direction LR
    api <--> db
  end

  ui <--> api

ユーザー認証やリレーションなど、アプリ開発に必要な知識は沢山ありますので、引き続き精進しようと思います。