SvelteKitで作るフルスタック個人ブログ | 第4回: SvelteKitとDrizzle ORMでバックエンド構築

こんにちは！「SvelteKitで作るフルスタック個人ブログ」シリーズの第4回へようこそ。第3回では、**Svelte-Query**を使って外部API（JSONPlaceholder）から記事データを取得し、ブログを動的に表示しました。今回は、SvelteKitの**APIルート**と**Drizzle ORM**を使って、独自のバックエンドを構築します。SQLiteデータベースを利用してブログ記事を保存・取得し、フルスタックアプリケーションの基盤を整えます。Next.jsとの比較も交えながら、SvelteKitのバックエンド開発のシンプルさを体感しましょう！

---

## この記事の目標
- SvelteKitの**APIルート**を使ってバックエンドエンドポイントを作成する。
- **Drizzle ORM**を導入し、SQLiteデータベースで記事データを管理する。
- 記事の作成・取得（CRUDの一部）を実装する。
- フロントエンドを更新して、独自のバックエンドからデータを取得。
- Next.jsのAPIルートとの違いを比較し、SvelteKitの開発体験（DX）の良さを確認する。

最終的には、JSONPlaceholderをやめて、プロジェクト内で完結するバックエンドを構築し、ブログ記事をデータベースから動的に表示します。では、始めましょう！

---

## Drizzle ORMとは？

**Drizzle ORM**は、軽量で型安全なJavaScript/TypeScript向けのORM（Object-Relational Mapping）です。Prismaに似ていますが、以下のような特徴があります：
- **軽量**：Prismaより依存関係が少なく、ビルドサイズが小さい。
- **型安全**：TypeScriptとの統合が強力で、データベース操作を安全に記述。
- **シンプルなAPI**：SQLライクなクエリビルダーで、直感的に操作可能。

SvelteKitのAPIルートと組み合わせることで、簡単にバックエンドを構築できます。Next.jsでも同様のことは可能ですが、SvelteKitのAPIルートは軽量で、Drizzle ORMとの相性が抜群です。

---

## バックエンドのセットアップ

### 1. Drizzle ORMとSQLiteのインストール
プロジェクトにDrizzle ORMとSQLiteを追加します。以下のコマンドを実行：

```bash
npm install drizzle-orm better-sqlite3
npm install --save-dev drizzle-kit
```

- `better-sqlite3`：軽量なSQLiteライブラリ。開発環境で簡単に使えます。
- `drizzle-kit`：スキーマのマイグレーションや管理ツール。

### 2. データベーススキーマの定義
ブログ記事を保存するため、`posts`テーブルを定義します。`src/lib/db/schema.ts`を作成：

```ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  slug: text('slug').notNull().unique(),
  excerpt: text('excerpt').notNull(),
  content: text('content').notNull(),
  createdAt: text('created_at').notNull().default(new Date().toISOString())
});
```

- **解説**：
  - `sqliteTable`で`posts`テーブルを定義。
  - `id`：自動インクリメントの主キー。
  - `slug`：ユニークな識別子（URL用）。
  - `createdAt`：デフォルトで現在の日時を保存。

### 3. データベース接続の設定
データベース接続を管理するため、`src/lib/db/index.ts`を作成：

```ts
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';

const sqlite = new Database('sqlite.db');
export const db = drizzle(sqlite, { schema });
```

- SQLiteデータベースファイル（`sqlite.db`）をプロジェクトルートに作成。
- `drizzle`関数でDrizzle ORMを初期化し、スキーマを適用。

### 4. マイグレーションの実行
Drizzle Kitを使って、スキーマをデータベースに適用します。`drizzle.config.ts`をプロジェクトルートに作成：

```ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/lib/db/schema.ts',
  out: './drizzle',
  driver: 'better-sqlite',
  dbCredentials: {
    url: 'sqlite.db'
  }
});
```

マイグレーションを実行：

```bash
npx drizzle-kit generate
npx drizzle-kit migrate
```

これで、`posts`テーブルがSQLiteデータベースに作成されます。

---

## APIルートの作成

SvelteKitのAPIルートを使って、記事の取得と作成を実装します。

### 1. 記事一覧の取得（GET）
`src/routes/api/posts/+server.ts`を作成：

```ts
import { json } from '@sveltejs/kit';
import { db } from '$lib/db';
import { posts } from '$lib/db/schema';

export async function GET() {
  const allPosts = await db.select().from(posts).all();
  return json(allPosts);
}
```

- **解説**：
  - `db.select().from(posts)`で全記事を取得。
  - `json`ヘルパーでデータをJSON形式で返す。

### 2. 記事の作成（POST）
同じファイルにPOSTハンドラを追加：

```ts
import { json } from '@sveltejs/kit';
import { db } from '$lib/db';
import { posts } from '$lib/db/schema';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async () => {
  const allPosts = await db.select().from(posts).all();
  return json(allPosts);
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();
  const newPost = {
    title: body.title,
    slug: body.slug,
    excerpt: body.excerpt,
    content: body.content,
    createdAt: new Date().toISOString()
  };

  await db.insert(posts).values(newPost);
  return json({ message: '記事を作成しました' }, { status: 201 });
};
```

- **解説**：
  - `POST`リクエストのボディから記事データを受け取る。
  - `db.insert(posts).values()`でデータベースに保存。
  - 成功時に201ステータスを返す。

### 3. 記事詳細の取得（GET）
`src/routes/api/posts/[slug]/+server.ts`を作成：

```ts
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/db';
import { posts } from '$lib/db/schema';
import { eq } from 'drizzle-orm';

export async function GET({ params }) {
  const post = await db.select().from(posts).where(eq(posts.slug, params.slug)).get();
  if (!post) {
    throw error(404, '記事が見つかりません');
  }
  return json(post);
}
```

- **解説**：
  - `eq(posts.slug, params.slug)`で指定された`slug`の記事を取得。
  - 記事が見つからない場合は404エラーを返す。

### 4. Next.jsとの比較
Next.jsで同様のAPIルートを作る場合、例えば：

```ts
// Next.js (pages/api/posts.ts)
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    // データベースから取得
    res.status(200).json(posts);
  } else if (req.method === 'POST') {
    // データベースに保存
    res.status(201).json({ message: 'Created' });
  }
}
```

Next.jsのAPIルートも強力ですが、SvelteKitのAPIルートは**型安全なハンドラ**（`RequestHandler`）と`json`ヘルパーで、より簡潔に書けます。また、Drizzle ORMのSQLライクなクエリは、Prismaより軽量で、SvelteKitの軽快な開発体験にマッチします。

---

## フロントエンドの更新

バックエンドが完成したので、フロントエンドを更新して独自のAPIからデータを取得します。

### 1. 記事一覧ページの修正
`src/routes/blog/+page.svelte`を以下のように更新：

```svelte
<script>
  import { useQuery } from '@sveltestack/svelte-query';
  import { Card, CardHeader, CardContent, CardFooter, Spinner } from '@skeletonlabs/skeleton';

  const query = useQuery('posts', async () => {
    const response = await fetch('/api/posts');
    if (!response.ok) throw new Error('データの取得に失敗しました');
    return response.json();
  });
</script>

<div class="container mx-auto p-8">
  <h1 class="h1 mb-8">ブログ記事一覧</h1>

  {#if $query.isLoading}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $query.isError}
    <p class="text-error-500">エラー: {$query.error.message}</p>
  {:else}
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
      {#each $query.data as post}
        <Card>
          <CardHeader>
            <h2 class="h3">{post.title}</h2>
            <p class="text-sm text-surface-500">{post.createdAt}</p>
          </CardHeader>
          <CardContent>
            <p>{post.excerpt}</p>
          </CardContent>
          <CardFooter>
            <a href="/blog/{post.slug}" class="btn variant-filled-primary">続きを読む</a>
          </CardFooter>
        </Card>
      {/each}
    </div>
  {/if}
</div>
```

- **変更点**：
  - `fetch`のURLを`/api/posts`に変更。
  - `createdAt`を日付として表示。

### 2. 記事詳細ページの修正
`src/routes/blog/[slug]/+page.svelte`を更新：

```svelte
<script>
  import { useQuery } from '@sveltestack/svelte-query';
  import { Spinner } from '@skeletonlabs/skeleton';
  import { error } from '@sveltejs/kit';

  export let data;

  const query = useQuery(['post', data.slug], async () => {
    const response = await fetch(`/api/posts/${data.slug}`);
    if (!response.ok) throw error(404, '記事が見つかりません');
    return response.json();
  });
</script>

<div class="container mx-auto p-8">
  {#if $query.isLoading}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $query.isError}
    <p class="text-error-500">エラー: {$query.error.message}</p>
  {:else}
    <article class="card p-6 bg-surface-50">
      <header class="mb-4">
        <h1 class="h2">{$query.data.title}</h1>
        <p class="text-sm text-surface-500">{$query.data.createdAt}</p>
      </header>
      <section class="prose">
        <p>{$query.data.content}</p>
      </section>
      <footer class="mt-6">
        <a href="/blog" class="btn variant-ghost-primary">一覧に戻る</a>
      </footer>
    </article>
  {/if}
</div>
```

- **変更点**：
  - `fetch`のURLを`/api/posts/${data.slug}`に変更。
  - データベースのフィールド（`content`, `createdAt`）に合わせて表示。

---

## 動作確認とデータ追加

プロジェクトを起動：

```bash
npm run dev
```

### 1. テストデータの追加
データベースが空なので、テスト記事を追加します。ターミナルで以下を実行：

```bash
node
```

```js
const { db } = require('./src/lib/db');
const { posts } = require('./src/lib/db/schema');

db.insert(posts).values([
  {
    title: 'SvelteKitでブログを始める',
    slug: 'start-sveltekit-blog',
    excerpt: 'SvelteKitを使ったブログ構築の第一歩。',
    content: 'この記事では、SvelteKitの基本を学びます...',
    createdAt: new Date().toISOString()
  },
  {
    title: 'Drizzle ORMの魅力',
    slug: 'drizzle-orm-intro',
    excerpt: '軽量で型安全なORMを試してみよう。',
    content: 'Drizzle ORMは、Prismaより軽量で...',
    createdAt: new Date().toISOString()
  }
]).run();
```

`Ctrl+D`で終了。

### 2. ブラウザで確認
- `http://localhost:5173/blog`：データベースから取得した記事一覧が表示。
- `http://localhost:5173/blog/start-sveltekit-blog`：指定した記事の詳細ページ。

**Skeleton UI**のカードデザインと**Svelte-Query**のキャッシュ管理により、UIはスムーズでモダンです。Drizzle ORMの型安全なクエリのおかげで、データベース操作も安心です。

---

## やってみよう！（チャレンジ）
記事を検索するAPIエンドポイントを追加してみましょう！例えば、`src/routes/api/posts/+server.ts`に検索機能を追加：

```ts
export const GET: RequestHandler = async ({ url }) => {
  const query = url.searchParams.get('q');
  if (query) {
    const filteredPosts = await db
      .select()
      .from(posts)
      .where(like(posts.title, `%${query}%`))
      .all();
    return json(filteredPosts);
  }
  const allPosts = await db.select().from(posts).all();
  return json(allPosts);
};
```

これを試すには、`fetch('/api/posts?q=Svelte')`でタイトルに「Svelte」を含む記事を検索できます。フロントエンドに検索バーを追加して、ユーザーがキーワードで記事を絞り込めるように挑戦してみてください！

---

## まとめ
この記事では、SvelteKitのAPIルートと**Drizzle ORM**を使って、ブログ記事を保存・取得するバックエンドを構築しました。**Svelte-Query**を活用して、フロントエンドを独自のAPIに接続し、フルスタックアプリケーションの基盤を完成させました。Next.jsと比べ、SvelteKitのAPIルートはシンプルで、Drizzle ORMの軽量さがプロジェクトにマッチしていると感じていただけたと思います。

次回は、**Lucia**を使って認証機能を追加し、ユーザーがログインして新しい記事を作成できるようにします。お楽しみに！

この記事が役に立ったと思ったら、**LGTM**や**ストック**していただけると励みになります！質問や改善アイデアがあれば、コメントで教えてください。次の記事でまたお会いしましょう！