SvelteKitとGraphQLで効率的なデータフェッチングを実現

こんにちは！SvelteKitはREST APIとの統合が得意ですが、**GraphQL**を使うことで、さらに柔軟で効率的なデータフェッチングが可能です。この記事では、SvelteKitと**urql**ライブラリを使って、GraphQL APIからブログ記事を取得する方法を解説します。コード例を交えながら、シンプルで型安全なデータ取得を体験しましょう！

---

## なぜGraphQLか？

GraphQLは、必要なデータだけをリクエストできるAPI仕様で、以下のような利点があります：
- **柔軟性**：クライアントが必要なフィールドだけを指定。
- **型安全**：スキーマベースで、TypeScriptとの相性が良い。
- **単一エンドポイント**：複数のRESTエンドポイントを管理する必要なし。

SvelteKitでGraphQLを使うと、**urql**のような軽量ライブラリで簡単にデータフェッチングができます。ReactのApollo Clientに比べ、urqlは設定がシンプルで、SvelteKitの軽快な開発体験にマッチします。

---

## プロジェクトのセットアップ

新しいSvelteKitプロジェクトを作成：

```bash
npm create svelte@latest graphql-blog
cd graphql-blog
npm install
```

**urql**と関連パッケージをインストール：

```bash
npm install @urql/svelte graphql
```

**Skeleton UI**を追加：

```bash
npm install @skeletonlabs/skeleton @skeletonlabs/tw-plugin --save-dev
```

この記事では、[GraphQL Hive](https://graphqlhive.com/)のような公開GraphQL APIを仮に使用しますが、実際には自分のバックエンド（例：Hasura）を推奨します。

---

## GraphQLの統合

### 1. urqlクライアントの設定
`src/lib/graphql-client.ts`を作成：

```ts
import { createClient, defaultExchanges } from '@urql/svelte';

export const client = createClient({
  url: 'https://api.graphqlhive.com/graphql', // 仮のGraphQLエンドポイント
  exchanges: defaultExchanges
});
```

- **解説**：
  - `createClient`でGraphQLエンドポイントを設定。
  - `defaultExchanges`でキャッシュやフェッチングを有効化。

### 2. ブログ記事の取得
ブログ記事を取得するクエリを定義。`src/lib/queries.ts`：

```ts
import { gql } from '@urql/svelte';

export const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      slug
      excerpt
      createdAt
    }
  }
`;

export const GET_POST = gql`
  query GetPost($slug: String!) {
    post(slug: $slug) {
      id
      title
      slug
      excerpt
      content
      createdAt
    }
  }
`;
```

### 3. ブログ一覧ページの実装
`src/routes/+page.svelte`を更新：

```svelte
<script>
  import { operationStore, query } from '@urql/svelte';
  import { GET_POSTS } from '$lib/queries';
  import { Card, CardHeader, CardContent, CardFooter, Spinner } from '@skeletonlabs/skeleton';

  const posts = operationStore(GET_POSTS);
  query(posts);
</script>

<div class="container mx-auto p-8">
  <h1 class="h1 mb-8">ブログ記事一覧</h1>
  {#if $posts.fetching}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $posts.error}
    <p class="text-error-500">エラー: {$posts.error.message}</p>
  {:else}
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
      {#each $posts.data.posts 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>
```

### 4. 記事詳細ページの実装
`src/routes/blog/[slug]/+page.svelte`を作成：

```svelte
<script>
  import { operationStore, query } from '@urql/svelte';
  import { GET_POST } from '$lib/queries';
  import { Spinner, Card } from '@skeletonlabs/skeleton';

  export let data;
  const post = operationStore(GET_POST, { slug: data.slug });
  query(post);
</script>

<svelte:head>
  {#if !$post.fetching && !$post.error}
    <title>{$post.data.post.title} | My Blog</title>
    <meta name="description" content={$post.data.post.excerpt} />
  {/if}
</svelte:head>

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

`src/routes/blog/[slug]/+page.ts`：

```ts
export function load({ params }) {
  return {
    slug: params.slug
  };
}
```

### 5. 動作確認
プロジェクトを起動：

```bash
npm run dev
```

以下のURLを試す：
- `http://localhost:5173`：GraphQLから取得した記事一覧。
- `http://localhost:5173/blog/your-slug`：記事詳細ページ。

**Skeleton UI**のカードデザインで、記事がモダンに表示されます。ReactのApollo Clientでは、プロバイダーの設定やキャッシュ管理が複雑ですが、urqlは軽量で、SvelteKitのストアと自然に統合されます。

---

## やってみよう！（チャレンジ）
GraphQLのミューテーションを使って、記事を投稿するフォームを追加してみましょう！以下のようなミューテーションを定義：

```ts
export const CREATE_POST = gql`
  mutation CreatePost($title: String!, $slug: String!, $excerpt: String!, $content: String!) {
    createPost(title: $title, slug: $slug, excerpt: $excerpt, content: $content) {
      id
      title
      slug
    }
  }
`;
```

`src/routes/create/+page.svelte`でフォームを実装し、投稿後に一覧ページにリダイレクトしてみてください。また、urqlの`@urql/exchange-graphcache`を導入して、キャッシュを自動更新する機能を試してみましょう！

---

## まとめ
この記事では、SvelteKitと**urql**を使って、GraphQL APIからブログ記事を効率的に取得しました。**Skeleton UI**でモダンなUIを簡単に作り、型安全なデータフェッチングを実現しました。ReactのApollo Clientと比べ、urqlは設定が少なく、SvelteKitの軽快な開発体験を最大限に活かせます。

この記事が役に立ったと思ったら、**LGTM**や**ストック**していただけると嬉しいです！GraphQLで挑戦したいことや質問があれば、コメントで教えてください。また会いましょう！