APIルート設計

SvelteKitの+server.tsファイルを使用して、RESTful APIエンドポイントを構築する方法を学びます。型安全性を保ちながら、効率的なAPIを設計します。

APIルートの基本

SvelteKitでは、+server.tsファイルでAPIエンドポイントを定義します。

ファイル構造とURL

src/routes/
├── api/
│   ├── posts/
│   │   └── +server.ts      → /api/posts
│   └── posts/[id]/
│       └── +server.ts      → /api/posts/:id
null

基本的な実装

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

export const GET: RequestHandler = async () => {
  const posts = await fetchPosts();
  return json(posts);
};

export const POST: RequestHandler = async ({ request }) => {
  const data = await request.json();
  const post = await createPost(data);
  return json(post, { status: 201 });
};
typescript

SvelteKitのアーキテクチャ

SvelteKitは従来のMVC(Model-View-Controller)とは異なるアプローチを採用しています。

MVCパターンとの対応

従来のMVCSvelteKitの対応役割
Controller+server.tsの各関数HTTPリクエストの処理
Service$lib/server/のモジュールビジネスロジック
Model$lib/server/db/などデータアクセス層
View+page.svelteUIコンポーネント

責務の分離

大規模なアプリケーションでは、責務を適切に分離することが重要です。

// $lib/server/services/post.service.ts(サービス層)
import { db } from '$lib/server/db';
import type { Post, CreatePostDTO } from '$lib/types';

export class PostService {
  async findAll(): Promise<Post[]> {
    return await db.posts.findMany({
      orderBy: { createdAt: 'desc' }
    });
  }
  
  async findById(id: string): Promise<Post | null> {
    return await db.posts.findUnique({
      where: { id }
    });
  }
  
  async create(data: CreatePostDTO): Promise<Post> {
    return await db.posts.create({ data });
  }
  
  async update(id: string, data: Partial<CreatePostDTO>): Promise<Post> {
    return await db.posts.update({
      where: { id },
      data
    });
  }
  
  async delete(id: string): Promise<void> {
    await db.posts.delete({
      where: { id }
    });
  }
}
typescript
// src/routes/api/posts/+server.ts(コントローラー層)
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { PostService } from '$lib/server/services/post.service';

const postService = new PostService();

export const GET: RequestHandler = async () => {
  try {
    const posts = await postService.findAll();
    return json(posts);
  } catch (e) {
    throw error(500, 'データベースエラー');
  }
};

export const POST: RequestHandler = async ({ request }) => {
  try {
    const data = await request.json();
    const post = await postService.create(data);
    return json(post, { status: 201 });
  } catch (e) {
    throw error(400, '不正なリクエスト');
  }
};
typescript

このように、SvelteKitでも責務の分離は可能ですが、フレームワークが強制するのではなく、開発者が必要に応じて構造化します。

HTTPメソッドの実装

各HTTPメソッドに対応する関数を実装できます。

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

// GET /api/posts/:id
export const GET: RequestHandler = async ({ params }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id }
  });
  
  if (!post) {
    throw error(404, 'Post not found');
  }
  
  return json(post);
};

// PUT /api/posts/:id
export const PUT: RequestHandler = async ({ params, request }) => {
  const data = await request.json();
  const post = await db.posts.update({
    where: { id: params.id },
    data
  });
  return json(post);
};

// PATCH /api/posts/:id
export const PATCH: RequestHandler = async ({ params, request }) => {
  const data = await request.json();
  const post = await db.posts.update({
    where: { id: params.id },
    data
  });
  return json(post);
};

// DELETE /api/posts/:id
export const DELETE: RequestHandler = async ({ params }) => {
  await db.posts.delete({
    where: { id: params.id }
  });
  return new Response(null, { status: 204 });
};
typescript

リクエストとレスポンス

リクエストの処理

export const POST: RequestHandler = async ({ request, url, params, cookies }) => {
  // JSONボディの取得
  const body = await request.json();
  
  // フォームデータの取得
  const formData = await request.formData();
  
  // URLパラメータ
  const searchParams = url.searchParams;
  const page = searchParams.get('page') || '1';
  
  // ルートパラメータ
  const { id } = params;
  
  // Cookie
  const sessionId = cookies.get('sessionId');
  
  // ヘッダー
  const contentType = request.headers.get('content-type');
  
  return json({ success: true });
};
typescript

レスポンスの作成

// JSONレスポンス
return json({ data: 'value' });

// ステータスコード付き
return json({ created: true }, { status: 201 });

// カスタムヘッダー
return json(data, {
  headers: {
    'Cache-Control': 'max-age=3600'
  }
});

// テキストレスポンス
return new Response('Plain text', {
  headers: {
    'Content-Type': 'text/plain'
  }
});

// リダイレクト
return new Response(null, {
  status: 302,
  headers: {
    Location: '/login'
  }
});
typescript

エラーハンドリング

適切なエラーハンドリングで堅牢なAPIを構築

import { error } from '@sveltejs/kit';

export const GET: RequestHandler = async ({ params }) => {
  // バリデーション
  if (!params.id) {
    throw error(400, 'ID is required');
  }
  
  try {
    const post = await db.posts.findUnique({
      where: { id: params.id }
    });
    
    if (!post) {
      throw error(404, {
        message: 'Post not found',
        code: 'POST_NOT_FOUND'
      });
    }
    
    return json(post);
  } catch (e) {
    // データベースエラー
    console.error('Database error:', e);
    throw error(500, 'Internal server error');
  }
};
typescript

認証と認可

APIルートで認証を実装

// $lib/server/auth.ts
import { error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';

export async function requireAuth(event: RequestEvent) {
  const token = event.request.headers.get('Authorization')?.replace('Bearer ', '');
  
  if (!token) {
    throw error(401, 'Unauthorized');
  }
  
  const user = await verifyToken(token);
  if (!user) {
    throw error(401, 'Invalid token');
  }
  
  return user;
}

// 使用例
export const GET: RequestHandler = async (event) => {
  const user = await requireAuth(event);
  
  // 認証済みユーザーのみアクセス可能
  const posts = await db.posts.findMany({
    where: { userId: user.id }
  });
  
  return json(posts);
};
typescript

CORS設定

Cross-Origin Resource Sharingの設定

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  // APIルートのみCORSを適用
  if (event.url.pathname.startsWith('/api')) {
    // プリフライトリクエストの処理
    if (event.request.method === 'OPTIONS') {
      return new Response(null, {
        headers: {
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization'
        }
      });
    }
  }
  
  const response = await resolve(event);
  
  // APIレスポンスにCORSヘッダーを追加
  if (event.url.pathname.startsWith('/api')) {
    response.headers.append('Access-Control-Allow-Origin', '*');
  }
  
  return response;
};
typescript

レート制限

APIの過負荷を防ぐレート制限の実装

// $lib/server/rateLimit.ts
const attempts = new Map<string, { count: number; resetAt: number }>();

export function rateLimit(maxAttempts = 10, windowMs = 60000) {
  return async (event: RequestEvent) => {
    const ip = event.getClientAddress();
    const now = Date.now();
    
    const record = attempts.get(ip);
    
    if (!record || record.resetAt < now) {
      attempts.set(ip, { count: 1, resetAt: now + windowMs });
      return;
    }
    
    if (record.count >= maxAttempts) {
      throw error(429, 'Too many requests');
    }
    
    record.count++;
  };
}

// 使用例
const limiter = rateLimit(10, 60000); // 1分間に10リクエストまで

export const POST: RequestHandler = async (event) => {
  await limiter(event);
  // APIの処理
};
typescript

ベストプラクティス

1. 型安全性の確保

// $lib/types/api.ts
export interface CreatePostDTO {
  title: string;
  content: string;
  tags?: string[];
}

export interface PostResponse {
  id: string;
  title: string;
  content: string;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

// 使用
export const POST: RequestHandler = async ({ request }) => {
  const data: CreatePostDTO = await request.json();
  // 型チェックされたデータ処理
};
typescript

2. バリデーション

// Zodを使用した例
import { z } from 'zod';

const createPostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(10),
  tags: z.array(z.string()).optional()
});

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();
  
  try {
    const data = createPostSchema.parse(body);
    // バリデーション済みのデータを使用
  } catch (e) {
    if (e instanceof z.ZodError) {
      throw error(400, e.errors);
    }
    throw error(500, 'Internal error');
  }
};
typescript

3. ログとモニタリング

// $lib/server/logger.ts
export function logAPIRequest(event: RequestEvent, status: number, duration: number) {
  console.log({
    method: event.request.method,
    path: event.url.pathname,
    status,
    duration,
    timestamp: new Date().toISOString()
  });
}

// 使用
export const GET: RequestHandler = async (event) => {
  const start = Date.now();
  
  try {
    const data = await fetchData();
    const response = json(data);
    logAPIRequest(event, 200, Date.now() - start);
    return response;
  } catch (e) {
    logAPIRequest(event, 500, Date.now() - start);
    throw error(500, 'Internal error');
  }
};
typescript

まとめ

SvelteKitのAPIルートは、

  • シンプルで直感的: ファイルベースのルーティング
  • 型安全: TypeScriptとの完全な統合
  • 柔軟な設計: MVCパターンの適用も可能
  • フルスタック: フロントエンドとバックエンドの統合

次のステップ

Last update at: 2025/09/16 01:10:33