データ管理アプリケーションの開発 (Svelte & JSONBin API)

SvelteKitを用いて、JSONBin APIでJSONデータを管理し、ユーザーを追加・削除できるシンプルなアプリを実装しました。

リンク:https://fetchjsondata.pages.dev/

フロントおよびバックエンドにSvelteKit、APIはJSONBin.ioを用いました。JSONBin.ioのAPIを用いることでJSONBin.ioのクラウドにホスティングされたJSONデータをAPIを通してHTTPリクエストメソッドによる取得、編集することができます。

サーバーから取得したデータはフロントで指定した条件にフィルタリングすることで特定のデータを検索することができます。また、データの追加、削除にも対応しました。追加、削除処理にはパスワードによる認証を行います。これらの処理に必要なAPIキーとパスワードはサーバーサイドのみで静的なプライベート環境変数によって管理されているのでクライアント側に公開されることはありません。

JSONBin.ioのようなサービスを使えば、素早く簡単にデータの取得、編集を行うことができます。

環境変数の設定

このコードでは、環境変数を利用してAPIキーを管理します。$env/static/private を使用することで、環境変数をサーバーサイドで安全に利用できます。

import { VITE_API_KEY } from "$env/static/private";
import { MASTER_PASSWORD } from "$env/static/private";
import type { JSONBin } from "$lib/server/types";
import { error, json } from "@sveltejs/kit";
  • VITE_API_KEY:JSONBin APIのキー
  • MASTER_PASSWORD:認証に使用するパスワード

APIエンドポイントの設定

JSONBinのエンドポイント、APIキー、各リクエストで必要なパスワードを定義します。

const endpoint = "https://api.jsonbin.io/v3/b/000000000000000000000000";
const key = VITE_API_KEY;
const masterPassword = MASTER_PASSWORD;

GETリクエスト - ユーザー一覧の取得

GETリクエストでは、JSONBinからデータを取得し、ユーザー一覧を返します。

export async function GET() {
  const response = await fetch(`${endpoint}`, {
    method: "GET",
    headers: { "X-Master-Key": key },
  });

  if (!response.ok) {
    throw error(response.status, "Failed to fetch users");
  }

  const data = await response.json();
  return json({ users: data.record.users });
}

POSTリクエスト - ユーザーの追加

POSTリクエストでは、新しいユーザーを追加します。リクエストのヘッダーにパスワードを含め、認証を行います。

export async function POST({ request }) {
  const { id, name, age } = await request.json();
  if (!(id && name && age)) {
    return json({ success: false, message: "Invalid data" }, { status: 400 });
  }

  const password = request.headers.get("X-Password");
  if (password !== masterPassword) {
    return json({ success: false, message: "Invalid password" }, { status: 403 });
  }

  const response = await fetch(endpoint, {
    method: "GET",
    headers: { "X-Master-Key": key },
  });

  if (!response.ok) {
    throw error(response.status, "Failed to fetch users");
  }

  const data = await response.json();
  const users = data.record.users;
  if (users.find((user: JSONBin) => user.id === id)) {
    return new Response("ID already exists", { status: 409 });
  }

  const updatedUsers = [...users, { id, name, age }];
  const putResponse = await fetch(endpoint, {
    method: "PUT",
    headers: { "Content-Type": "application/json", "X-Master-Key": key },
    body: JSON.stringify({ users: updatedUsers }),
  });

  if (!putResponse.ok) {
    throw error(putResponse.status, "Failed to add user");
  }

  return json({ success: true });
}

DELETEリクエスト - ユーザーの削除

指定したIDのユーザーを削除するリクエストを処理します。

export async function DELETE({ request }) {
  const { id } = await request.json();
  if (!id) {
    return json({ success: false, message: "User ID is required" }, { status: 400 });
  }

  const password = request.headers.get("X-Password");
  if (password !== masterPassword) {
    return json({ success: false, message: "Invalid password" }, { status: 403 });
  }

  const getResponse = await fetch(endpoint, {
    method: "GET",
    headers: { "X-Master-Key": key },
  });

  if (!getResponse.ok) {
    throw error(getResponse.status, "Failed to fetch users");
  }

  const data = await getResponse.json();
  const users = data.record.users;
  if (!users.find((user: JSONBin) => user.id === id)) {
    return new Response("User not found", { status: 404 });
  }

  const updatedUsers = users.filter((user: JSONBin) => user.id !== id);
  const putResponse = await fetch(endpoint, {
    method: "PUT",
    headers: { "Content-Type": "application/json", "X-Master-Key": key },
    body: JSON.stringify({ users: updatedUsers }),
  });

  if (!putResponse.ok) {
    throw error(putResponse.status, "Failed to delete user");
  }

  return json({ success: true });
}

クライアントサイドコードの概要

1. 必要なモジュールとコンポーネントのインポート

<script lang="ts">
  import { goto } from "$app/navigation";
  import Card from "$lib/components/Card.svelte";
  import SearchForm from "$lib/components/SearchForm.svelte";
  import { onMount } from "svelte";
  • $app/navigationgoto() を使用して検索結果ページに遷移します。
  • Card.svelteSearchForm.svelte などのコンポーネントをインポートしています。

2. データの定義

  let query = "";
  let users: User[] = [];
  let newId = "", newName = "", newAge = "", deleteId = "", addPassword = "", deletePassword = "";

  interface User {
    id: string;
    name: string;
    age: number;
  }
  • users 配列でユーザー情報を管理します。
  • newId, newName, newAge はユーザー追加用のデータ。
  • deleteId は削除対象のIDを保持します。
  • addPassworddeletePassword はパスワード認証に使用します。

3. onMountを使用したデータ取得

  onMount(async () => {
    const response = await fetch("/");
    if (response.ok) {
      const data = await response.json();
      users = data.users;
    }
  });
  • onMount() を使い、ページがロードされた際にサーバーからユーザーリストを取得します。
  • fetch() を使用して JSON データを取得し、users に格納します。

4. ユーザーの追加処理

  async function addUser() {
    const response = await fetch("/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Password": addPassword,
      },
      body: JSON.stringify({
        id: newId,
        name: newName,
        age: Number.parseInt(newAge),
      }),
    });

    if (response.status === 409) {
      alert("このIDはすでに存在します");
    } else if (response.ok) {
      const updatedResponse = await fetch("/");
      if (updatedResponse.ok) {
        const updatedData = await updatedResponse.json();
        users = updatedData.users;
      }
      newId = "";
      newName = "";
      newAge = "";
      addPassword = "";
    } else {
      alert("パスワードが正しくありません。");
    }
  }
  • fetch()POST メソッドを使用し、新しいユーザーを追加します。
  • X-Password ヘッダーでパスワード認証を行います。
  • 成功時には、最新のデータを取得し、フォームをリセットします。

5. ユーザーの削除処理

  async function deleteUser() {
    const response = await fetch("/", {
      method: "DELETE",
      headers: {
        "Content-Type": "application/json",
        "X-Password": deletePassword,
      },
      body: JSON.stringify({ id: deleteId }),
    });

    if (response.status === 404) {
      alert("このIDは存在しません");
    } else if (response.ok) {
      const updatedResponse = await fetch("/");
      if (updatedResponse.ok) {
        const updatedData = await updatedResponse.json();
        users = updatedData.users;
      }
      deleteId = "";
      deletePassword = "";
    } else {
      alert("パスワードが正しくありません。");
    }
  }
  • DELETE メソッドを使用してユーザーを削除します。
  • 削除成功後に最新のデータを取得し、削除フォームをリセットします。

6. UI部分

    <SearchForm {query} on:search={handleSearch} />
    <dl class="users">
      <div class="title">
        <dt>ID</dt>
        <dt>Name</dt>
        <dt>Age</dt>
      </div>
      {#each users as user}
        <div class="data">
          <dd>{user.id}</dd>
          <dd>{user.name}</dd>
          <dd>{user.age}</dd>
        </div>
      {/each}
    </dl>
  • SearchForm コンポーネントを使用し、検索機能を実装。
  • {#each users as user}users のデータをループ表示。

7. フォームのスタイル

  .form {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fit, minmax(192px, auto));
    max-width: 400px;
    margin: auto;
  }
  input[type="text"], input[type="number"], input[type="password"] {
    width: 192px;
  }
  button {
    background: chartreuse;
    border-radius: 8px;
    padding: 0 8px;
    margin: 16px 0;
  }
  • フォームを grid レイアウトで整列。
  • ボタンのスタイルを適用。

まとめ

このプロジェクトでは、SvelteKitを用いてシンプルなユーザー管理機能を実装しました。SvelteKitのエンドポイントを利用してJSONBinのデータを操作する方法を紹介しました。環境変数を活用し、認証機能を組み込むことで、安全なユーザー管理を実現できます。