SvelteKitのAPIルートにCORS対応を追加した話

SvelteKitの +server.ts で作ったAPIを別オリジンから呼び出すには、CORSヘッダーの設定が必要です。


CORSの仕組み

ブラウザには同一オリジンポリシーがあり、異なるオリジン(ドメイン・ポート)へのリクエストはデフォルトでブロックされます。CORSはサーバー側がヘッダーで「このオリジンからのアクセスを許可する」と伝える仕組みです。

シンプルリクエストとプリフライトリクエスト

GET などの単純なリクエストはそのまま送られますが、Content-Type: application/jsonPOSTPUT / DELETE などの条件を超えたリクエストは、本番リクエストの前に OPTIONS リクエスト(プリフライト)を自動で送ります。

① ブラウザ → OPTIONS /api/posts  (「このリクエストを送っていい?」)
② サーバー → 204 + CORSヘッダー (「いいよ」)
③ ブラウザ → POST /api/posts     (本来のリクエスト)
④ サーバー → 201 + CORSヘッダー (レスポンス)

実装:+server.ts に個別に設定する

import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

const CORS_HEADERS = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type',
};

// プリフライトリクエストへの対応
export const OPTIONS: RequestHandler = () => {
  return new Response(null, { status: 204, headers: CORS_HEADERS });
};

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

  return json(result.results, { headers: CORS_HEADERS });
};

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

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

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

OPTIONS ハンドラを追加してプリフライトに応答するのがポイントです。


各ヘッダーの意味

ヘッダー 役割
Access-Control-Allow-Origin 許可するオリジン(* または特定URL)
Access-Control-Allow-Methods 許可するHTTPメソッド
Access-Control-Allow-Headers 許可するリクエストヘッダー
Access-Control-Allow-Credentials Cookie・認証情報の送信を許可するか

* ではなく特定オリジンを指定する場合

Access-Control-Allow-Origin: * はすべてのオリジンを許可しますが、CookieやAuthorizationヘッダーは送れません。認証が必要なAPIでは特定のオリジンを指定します。

const CORS_HEADERS = {
  'Access-Control-Allow-Origin': 'https://frontend.example.com',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  'Access-Control-Allow-Credentials': 'true',  // Cookieを送る場合
};

複数オリジンを許可する

Access-Control-Allow-Origin には1つのオリジンしか指定できないため、複数許可したい場合はリクエストの Origin ヘッダーを見て動的に返します。

const ALLOWED_ORIGINS = [
  'https://frontend.example.com',
  'http://localhost:5173',
];

export const GET: RequestHandler = async ({ request, platform }) => {
  const origin = request.headers.get('Origin') ?? '';
  const allowedOrigin = ALLOWED_ORIGINS.includes(origin) ? origin : '';

  const result = await platform!.env.DB
    .prepare('SELECT id, title FROM posts')
    .all();

  return json(result.results, {
    headers: {
      'Access-Control-Allow-Origin': allowedOrigin,
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  });
};

hooks.server.ts で全体に適用する方法

すべてのAPIルートにまとめてCORSを適用したい場合は hooks.server.tshandle で設定します。

const cors: Handle = async ({ event, resolve }) => {
  if (event.request.method === 'OPTIONS') {
    return new Response(null, {
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      },
    });
  }

  const response = await resolve(event);
  response.headers.set('Access-Control-Allow-Origin', '*');
  return response;
};

export const handle = sequence(cors, auth, logger);

まとめ

  • 別オリジンからAPIを呼ぶにはサーバー側でCORSヘッダーが必要
  • Content-Type: application/json のPOSTなどはプリフライト(OPTIONS)が先に飛ぶので、OPTIONS ハンドラも追加する
  • * を指定するとCookie・認証情報は送れないため、認証が必要なAPIは特定オリジンを指定する
  • 全ルートに適用するなら hooks.server.tshandle でまとめて設定する
← トップページに戻る