SvelteKitの+server.tsでAPIエンドポイントを作った話

SvelteKitでは +server.ts を使うとREST APIのエンドポイントを作れます。load 関数や actions ではカバーできない場面で使います。


+server.ts が必要な場面

load / actions で対応できる     → +page.server.ts を使う
それ以外(APIとして公開したい) → +server.ts を使う

具体的には:

  • 外部サービスからの Webhook の受信
  • JSONを返すAPIエンドポイント(外部から叩かれる)
  • ファイルダウンロード(CSV・PDFなど)
  • クライアントから fetch で直接叩くエンドポイント

① 基本的な書き方

HTTPメソッド名の関数をエクスポートします。

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

// GET /api/posts
export const GET: RequestHandler = async ({ platform }) => {
  const result = await platform!.env.DB
    .prepare('SELECT * FROM posts')
    .all();

  return json(result.results);
};

// POST /api/posts
export const POST: RequestHandler = async ({ request, platform }) => {
  const body = await request.json();

  await platform!.env.DB
    .prepare('INSERT INTO posts (title, content) VALUES (?, ?)')
    .bind(body.title, body.content)
    .run();

  return json({ success: true }, { status: 201 });
};

② クライアントから叩く

fetch+page.svelte<script> 内に書きます。「ボタン操作などユーザーのアクションに応じてデータを取得・更新したいとき」が主な用途です。ページ表示時に自動取得するだけなら load 関数を使う方が適切です。

<!-- src/routes/learn/+page.svelte -->
<script lang="ts">
  let apiPosts = $state<any[]>([]);

  async function fetchPostsFromApi() {
    const res = await fetch('/api/posts');
    apiPosts = await res.json();
  }
</script>

<button onclick={fetchPostsFromApi}>APIから記事を取得</button>

<ul>
  {#each apiPosts as post}
    <li>{post.title}</li>
  {/each}
</ul>
タイミング 書き方
ボタン押下時 onclick={fetchPostsFromApi} で呼ぶ
ページ表示時に自動取得 $effect(() => { fetchPostsFromApi(); }) で呼ぶ
別の処理の後 関数の中で await fetchPostsFromApi() と呼ぶ

③ エラーレスポンスを返す

IDで1件取得するなど、リソースが見つからない場合のエラー処理はURLごとに別ファイルに書きます。

src/routes/api/posts/+server.ts        ← 一覧・追加
src/routes/api/posts/[id]/+server.ts   ← 1件取得・エラー処理
// src/routes/api/posts/[id]/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

// GET /api/posts/1
export const GET: RequestHandler = async ({ params, platform }) => {
  const post = await platform!.env.DB
    .prepare('SELECT * FROM posts WHERE id = ?')
    .bind(params.id)
    .first();

  if (!post) {
    return json({ error: '記事が見つかりません' }, { status: 404 });
  }

  return json(post);
};

④ JSON 以外のレスポンスを返す(CSVダウンロード)

CSVなどJSON以外のレスポンスは new Response() を使って返します。

src/routes/api/posts/export/+server.ts   ← CSVダウンロード
// src/routes/api/posts/export/+server.ts
import type { RequestHandler } from './$types';

// GET /api/posts/export
export const GET: RequestHandler = async ({ platform }) => {
  const result = await platform!.env.DB
    .prepare('SELECT * FROM posts')
    .all();

  const csv = [
    'id,title',  // ヘッダー行
    ...result.results.map((p: any) => `${p.id},${p.title}`)
  ].join('\n');

  return new Response(csv, {
    headers: {
      'Content-Type': 'text/csv',
      'Content-Disposition': 'attachment; filename="posts.csv"',
    },
  });
};

CSVダウンロードは fetch ではなく <a href="..." download> で呼ぶのがシンプルです。

<a href="/api/posts/export" download>CSVダウンロード</a>

Content-Disposition: attachment がついているのでブラウザがダウンロードとして扱い、posts.csv として保存されます。


+page.server.ts(actions)と +server.ts の使い分け

+page.server.ts(actions) +server.ts
呼び出し元 同じページのフォーム どこからでも(外部含む)
use:enhance 使える 使えない
レスポンス形式 SvelteKit の形式 自由(JSON・CSV など)
向いているケース フォーム送信 API・Webhook・ファイル出力

フォーム送信には actions、それ以外のAPIには +server.ts と覚えておくとシンプルです。

← トップページに戻る