API/14 min read/January 22, 2025
Danylo Kabanov
Chief Technology Officer
#モダンなAPI設計パターン
優れたAPI設計は、スケーラブルで保守しやすいアプリケーションの基盤です。ここでは、モダンなRESTful APIを設計するための実践的なパターンとベストプラクティスを紹介します。
##リソース指向の設計
APIをリソース中心に設計し、明確で一貫性のあるエンドポイントを作成します。
GET /api/users # ユーザー一覧の取得
GET /api/users/:id # 特定ユーザーの取得
POST /api/users # 新規ユーザーの作成
PUT /api/users/:id # ユーザー情報の更新
PATCH /api/users/:id # ユーザー情報の部分更新
DELETE /api/users/:id # ユーザーの削除
# ネストされたリソース
GET /api/users/:id/posts # ユーザーの投稿一覧
GET /api/posts/:id/comments # 投稿のコメント一覧
##バージョニング戦略
API の進化に備えてバージョニングを実装します。
typescript
1// URLパスによるバージョニング(推奨)2app.get('/api/v1/users', v1UsersHandler)3app.get('/api/v2/users', v2UsersHandler)45// ヘッダーによるバージョニング6app.use((req, res, next) => {7 const version = req.headers['api-version'] || 'v1'8 req.apiVersion = version9 next()10})1112// コンテンツネゴシエーション13app.get('/api/users', (req, res) => {14 const accept = req.headers['accept']15 if (accept.includes('application/vnd.api.v2+json')) {16 return v2UsersHandler(req, res)17 }18 return v1UsersHandler(req, res)19})
##ページネーション
大量のデータを効率的に処理するためのページネーション戦略。
typescript
1// カーソルベースのページネーション(推奨)2interface PaginatedResponse<T> {3 data: T[]4 pageInfo: {5 hasNextPage: boolean6 hasPreviousPage: boolean7 startCursor: string | null8 endCursor: string | null9 }10}1112async function getUsers(13 cursor?: string,14 limit: number = 2015): Promise<PaginatedResponse<User>> {16 const query = cursor17 ? { createdAt: { $lt: decodeCursor(cursor) } }18 : {}1920 const users = await User.find(query)21 .sort({ createdAt: -1 })22 .limit(limit + 1)2324 const hasNextPage = users.length > limit25 if (hasNextPage) users.pop()2627 return {28 data: users,29 pageInfo: {30 hasNextPage,31 hasPreviousPage: !!cursor,32 startCursor: users[0] ? encodeCursor(users[0].createdAt) : null,33 endCursor: users[users.length - 1]34 ? encodeCursor(users[users.length - 1].createdAt)35 : null,36 },37 }38}3940// オフセットベースのページネーション41interface OffsetPaginatedResponse<T> {42 data: T[]43 pagination: {44 page: number45 pageSize: number46 totalCount: number47 totalPages: number48 }49}
##エラーハンドリング
一貫性のあるエラーレスポンス形式を定義します。
typescript
1interface ApiError {2 error: {3 code: string4 message: string5 details?: Record<string, any>6 timestamp: string7 path: string8 }9}1011// グローバルエラーハンドラー12app.use((err: Error, req: Request, res: Response, next: NextFunction) => {13 const statusCode = err instanceof HttpError ? err.statusCode : 5001415 const errorResponse: ApiError = {16 error: {17 code: err.name || 'INTERNAL_SERVER_ERROR',18 message: err.message,19 details: err instanceof ValidationError ? err.details : undefined,20 timestamp: new Date().toISOString(),21 path: req.path,22 },23 }2425 // ログ記録26 logger.error({27 ...errorResponse,28 stack: err.stack,29 })3031 res.status(statusCode).json(errorResponse)32})3334// カスタムエラークラス35class ValidationError extends Error {36 constructor(public details: Record<string, string>) {37 super('Validation failed')38 this.name = 'VALIDATION_ERROR'39 }40}4142class NotFoundError extends Error {43 constructor(resource: string) {44 super(`${resource} not found`)45 this.name = 'NOT_FOUND'46 }47}
##レート制限
APIの乱用を防ぐためのレート制限を実装します。
typescript
1import rateLimit from 'express-rate-limit'23// 基本的なレート制限4const limiter = rateLimit({5 windowMs: 15 * 60 * 1000, // 15分6 max: 100, // 最大100リクエスト7 message: 'リクエストが多すぎます。後でもう一度お試しください。',8 standardHeaders: true,9 legacyHeaders: false,10})1112app.use('/api/', limiter)1314// エンドポイント固有のレート制限15const strictLimiter = rateLimit({16 windowMs: 60 * 60 * 1000, // 1時間17 max: 10, // 最大10リクエスト18})1920app.post('/api/auth/login', strictLimiter, loginHandler)2122// ユーザーベースのレート制限23const userLimiter = rateLimit({24 keyGenerator: (req) => req.user?.id || req.ip,25 max: 1000,26})
##キャッシング戦略
パフォーマンスを向上させるための効果的なキャッシング。
typescript
1import { Redis } from 'ioredis'23const redis = new Redis()45// キャッシュミドルウェア6function cacheMiddleware(duration: number) {7 return async (req: Request, res: Response, next: NextFunction) => {8 const key = `cache:${req.path}:${JSON.stringify(req.query)}`910 const cached = await redis.get(key)11 if (cached) {12 return res.json(JSON.parse(cached))13 }1415 const originalJson = res.json.bind(res)16 res.json = (data: any) => {17 redis.setex(key, duration, JSON.stringify(data))18 return originalJson(data)19 }2021 next()22 }23}2425// 使用例26app.get('/api/products', cacheMiddleware(300), getProducts)2728// ETags によるキャッシング29app.get('/api/users/:id', async (req, res) => {30 const user = await User.findById(req.params.id)31 const etag = generateEtag(user)3233 if (req.headers['if-none-match'] === etag) {34 return res.status(304).end()35 }3637 res.setHeader('ETag', etag)38 res.setHeader('Cache-Control', 'max-age=300')39 res.json(user)40})
##認証と認可
JWT を使用した安全な認証システム。
typescript
1import jwt from 'jsonwebtoken'23interface JWTPayload {4 userId: string5 role: string6 permissions: string[]7}89// トークン生成10function generateToken(user: User): string {11 const payload: JWTPayload = {12 userId: user.id,13 role: user.role,14 permissions: user.permissions,15 }1617 return jwt.sign(payload, process.env.JWT_SECRET!, {18 expiresIn: '24h',19 issuer: 'cyberwolf.studio',20 })21}2223// 認証ミドルウェア24function authenticate(req: Request, res: Response, next: NextFunction) {25 const token = req.headers.authorization?.replace('Bearer ', '')2627 if (!token) {28 return res.status(401).json({ error: 'トークンが必要です' })29 }3031 try {32 const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload33 req.user = decoded34 next()35 } catch (error) {36 return res.status(401).json({ error: '無効なトークンです' })37 }38}3940// 権限チェック41function authorize(...permissions: string[]) {42 return (req: Request, res: Response, next: NextFunction) => {43 if (!req.user) {44 return res.status(401).json({ error: '認証が必要です' })45 }4647 const hasPermission = permissions.some(p =>48 req.user!.permissions.includes(p)49 )5051 if (!hasPermission) {52 return res.status(403).json({ error: 'アクセスが拒否されました' })53 }5455 next()56 }57}5859// 使用例60app.delete(61 '/api/users/:id',62 authenticate,63 authorize('users:delete'),64 deleteUser65)
##リクエスト検証
入力データを検証してセキュリティを強化します。
typescript
1import { z } from 'zod'23// スキーマ定義4const createUserSchema = z.object({5 body: z.object({6 name: z.string().min(1).max(100),7 email: z.string().email(),8 password: z.string().min(8).max(100),9 role: z.enum(['admin', 'user']).optional(),10 }),11 query: z.object({}).optional(),12 params: z.object({}).optional(),13})1415// 検証ミドルウェア16function validate<T extends z.ZodType>(schema: T) {17 return async (req: Request, res: Response, next: NextFunction) => {18 try {19 await schema.parseAsync({20 body: req.body,21 query: req.query,22 params: req.params,23 })24 next()25 } catch (error) {26 if (error instanceof z.ZodError) {27 return res.status(400).json({28 error: {29 code: 'VALIDATION_ERROR',30 message: '入力データが無効です',31 details: error.errors,32 },33 })34 }35 next(error)36 }37 }38}3940// 使用例41app.post('/api/users', validate(createUserSchema), createUser)
##APIドキュメンテーション
OpenAPI/Swagger を使用した自動ドキュメント生成。
typescript
1import swaggerJsdoc from 'swagger-jsdoc'2import swaggerUi from 'swagger-ui-express'34const options = {5 definition: {6 openapi: '3.0.0',7 info: {8 title: 'CyberWolf.Studio API',9 version: '1.0.0',10 description: 'API documentation',11 },12 servers: [13 {14 url: 'https://api.cyberwolf.studio',15 description: 'Production server',16 },17 ],18 },19 apis: ['./src/routes/*.ts'],20}2122const specs = swaggerJsdoc(options)23app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs))2425/**26 * @swagger27 * /api/users:28 * get:29 * summary: ユーザー一覧を取得30 * tags: [Users]31 * parameters:32 * - in: query33 * name: page34 * schema:35 * type: integer36 * description: ページ番号37 * responses:38 * 200:39 * description: 成功40 */
##結論
優れたAPI設計は、一貫性、セキュリティ、パフォーマンス、開発者エクスペリエンスのバランスです。これらのパターンに従うことで、スケーラブルで保守しやすいAPIを構築できます。
***
API設計のコンサルティングが必要ですか?お問い合わせください。
APIRESTアーキテクチャバックエンド