さとまたwiki

APIエンドポイント

+server.ts でバックエンドAPIを作る

APIエンドポイントとは?

日常での例え:レストランの厨房への注文窓口

注文窓口

お客さん(フロントエンド)が
「カレーください」と注文
→ 厨房が調理して提供

APIエンドポイント

クライアントが
「/api/users」にリクエスト
→ サーバーがデータを返す

なぜAPIエンドポイントが必要?

APIエンドポイントがないと困ること:

  • データベースに直接アクセス?→ セキュリティ的に危険すぎる
  • 外部サービス(決済など)の呼び出し→ APIキーが丸見えになる
  • サーバーでしかできない処理が実行できない

APIエンドポイントがあれば:安全にデータのやり取りができる!
機密情報はサーバー側に隠したまま処理できます。

SvelteKitの良いところ

フロントエンドとバックエンドが同じプロジェクトで書ける!
別のサーバーを用意する必要がありません。

用語解説

API:アプリケーション同士がデータをやり取りするための窓口。

エンドポイント:APIの「住所」のこと。例:/api/users

GET:データを「取得」する時のリクエスト方法。

POST:データを「送信・作成」する時のリクエスト方法。

PUT / PATCH:データを「更新」する時のリクエスト方法。

DELETE:データを「削除」する時のリクエスト方法。

JSON:データ交換用の形式。{"name": "太郎", "age": 20} のような書き方。

ステータスコード:リクエストの結果を表す番号。200=成功、404=見つからない、500=サーバーエラー

+server.ts の場所

src/routes/api/users/+server.ts

/api/users でアクセス可能

基本的なAPI

GETリクエスト

データの取得

typescript
// src/routes/api/users/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

// GET /api/users
export const GET: RequestHandler = async () => {
  const users = [
    { id: 1, name: '田中太郎' },
    { id: 2, name: '鈴木花子' }
  ];

  return json(users);
};

// レスポンス:
// [{"id":1,"name":"田中太郎"},{"id":2,"name":"鈴木花子"}]
プレビュー
GET /api/users
// ユーザー一覧を返す

POSTリクエスト

データの作成

typescript
// src/routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

// POST /api/users
export const POST: RequestHandler = async ({ request }) => {
  // リクエストボディを取得
  const body = await request.json();

  // バリデーション
  if (!body.name || !body.email) {
    throw error(400, 'name と email は必須です');
  }

  // DBに保存(例)
  const newUser = await db.user.create({
    data: {
      name: body.name,
      email: body.email
    }
  });

  // 201 Created で返す
  return json(newUser, { status: 201 });
};
プレビュー
POST /api/users
// 新しいユーザーを作成

他のHTTPメソッド

PUT, PATCH, DELETE

typescript
// src/routes/api/users/[id]/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

// GET /api/users/:id
export const GET: RequestHandler = async ({ params }) => {
  const user = await db.user.findUnique({
    where: { id: Number(params.id) }
  });

  if (!user) {
    throw error(404, 'ユーザーが見つかりません');
  }

  return json(user);
};

// PUT /api/users/:id (全体更新)
export const PUT: RequestHandler = async ({ params, request }) => {
  const body = await request.json();
  const user = await db.user.update({
    where: { id: Number(params.id) },
    data: body
  });
  return json(user);
};

// PATCH /api/users/:id (部分更新)
export const PATCH: RequestHandler = async ({ params, request }) => {
  const body = await request.json();
  const user = await db.user.update({
    where: { id: Number(params.id) },
    data: body
  });
  return json(user);
};

// DELETE /api/users/:id
export const DELETE: RequestHandler = async ({ params }) => {
  await db.user.delete({
    where: { id: Number(params.id) }
  });
  return new Response(null, { status: 204 });
};
プレビュー
GET - 取得
PUT - 全体更新
PATCH - 部分更新
DELETE - 削除

リクエスト情報の取得

様々なリクエスト情報

event オブジェクトから取得

typescript
export const GET: RequestHandler = async (event) => {
  // URLパラメータ: /api/users/123 → params.id = "123"
  const { id } = event.params;

  // クエリパラメータ: /api/users?page=2&limit=10
  const page = event.url.searchParams.get('page') || '1';
  const limit = event.url.searchParams.get('limit') || '10';

  // リクエストヘッダー
  const authHeader = event.request.headers.get('Authorization');

  // Cookie
  const session = event.cookies.get('session');

  // ローカル(hooksで設定した値)
  const user = event.locals.user;

  return json({ page, limit });
};
プレビュー
params - URLパラメータ
url.searchParams - クエリ
request.headers - ヘッダー
cookies - Cookie

FormDataの処理

ファイルアップロードなど

typescript
// src/routes/api/upload/+server.ts
export const POST: RequestHandler = async ({ request }) => {
  const formData = await request.formData();

  // テキストフィールド
  const title = formData.get('title');

  // ファイル
  const file = formData.get('file') as File;

  if (file) {
    const buffer = await file.arrayBuffer();
    // ファイルを保存...
    console.log('ファイル名:', file.name);
    console.log('サイズ:', file.size);
    console.log('タイプ:', file.type);
  }

  return json({ success: true });
};
プレビュー
// FormDataでファイルを受け取る
// request.formData() で取得

レスポンスの種類

様々なレスポンス

JSON以外も返せる

typescript
import { json, text, error, redirect } from '@sveltejs/kit';

// JSON レスポンス
export const GET: RequestHandler = async () => {
  return json({ message: 'Hello' });
};

// テキスト レスポンス
export const GET: RequestHandler = async () => {
  return text('Hello, World!');
};

// エラー レスポンス
export const GET: RequestHandler = async () => {
  throw error(404, 'Not Found');
};

// リダイレクト
export const GET: RequestHandler = async () => {
  throw redirect(302, '/login');
};

// カスタムレスポンス
export const GET: RequestHandler = async () => {
  return new Response('<h1>HTML</h1>', {
    status: 200,
    headers: {
      'Content-Type': 'text/html'
    }
  });
};
プレビュー
json() - JSON
text() - テキスト
error() - エラー
redirect() - リダイレクト

フロントエンドからの呼び出し

fetchでAPIを呼ぶ

Svelteコンポーネントから

svelte
<script>
  let users = $state([]);
  let loading = $state(false);
  let error = $state('');

  async function loadUsers() {
    loading = true;
    error = '';

    try {
      const response = await fetch('/api/users');

      if (!response.ok) {
        throw new Error('データの取得に失敗しました');
      }

      users = await response.json();
    } catch (e) {
      error = e.message;
    } finally {
      loading = false;
    }
  }

  async function createUser() {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        name: '新しいユーザー',
        email: 'new@example.com'
      })
    });

    if (response.ok) {
      await loadUsers();  // リストを更新
    }
  }

  // 初回読み込み
  $effect(() => {
    loadUsers();
  });
</script>

{#if loading}
  <p>読み込み中...</p>
{:else if error}
  <p class="error">{error}</p>
{:else}
  <ul>
    {#each users as user}
      <li>{user.name}</li>
    {/each}
  </ul>
{/if}

<button onclick={createUser}>ユーザー追加</button>
プレビュー
  • 田中太郎
  • 鈴木花子

認証付きAPI

認証チェック

localsを使った認証

typescript
// src/routes/api/protected/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ locals }) => {
  // hooksで設定したユーザー情報をチェック
  if (!locals.user) {
    throw error(401, '認証が必要です');
  }

  // 権限チェック
  if (locals.user.role !== 'admin') {
    throw error(403, '権限がありません');
  }

  // 保護されたデータを返す
  const secretData = await getSecretData();
  return json(secretData);
};

// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
  const session = event.cookies.get('session');
  if (session) {
    event.locals.user = await getUserFromSession(session);
  }
  return resolve(event);
};
プレビュー
// hooks.server.ts でユーザーを設定
// API側で locals.user をチェック

ベストプラクティス

✅ やるべきこと

  • バリデーションを必ず行う
  • 適切なステータスコードを返す
  • エラーメッセージを分かりやすく
  • 型定義をしっかり書く

❌ 避けるべきこと

  • ユーザー入力を信用する
  • 機密情報をレスポンスに含める
  • エラーの詳細を本番で露出
  • 認証なしで機密データにアクセス