API

モダンなAPI設計パターン

cyberwolf.studio
API/14 min read/January 22, 2025
Danylo Kabanov
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)
4
5// ヘッダーによるバージョニング
6app.use((req, res, next) => {
7 const version = req.headers['api-version'] || 'v1'
8 req.apiVersion = version
9 next()
10})
11
12// コンテンツネゴシエーション
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: boolean
6 hasPreviousPage: boolean
7 startCursor: string | null
8 endCursor: string | null
9 }
10}
11
12async function getUsers(
13 cursor?: string,
14 limit: number = 20
15): Promise<PaginatedResponse<User>> {
16 const query = cursor
17 ? { createdAt: { $lt: decodeCursor(cursor) } }
18 : {}
19
20 const users = await User.find(query)
21 .sort({ createdAt: -1 })
22 .limit(limit + 1)
23
24 const hasNextPage = users.length > limit
25 if (hasNextPage) users.pop()
26
27 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}
39
40// オフセットベースのページネーション
41interface OffsetPaginatedResponse<T> {
42 data: T[]
43 pagination: {
44 page: number
45 pageSize: number
46 totalCount: number
47 totalPages: number
48 }
49}

##エラーハンドリング

一貫性のあるエラーレスポンス形式を定義します。

typescript
1interface ApiError {
2 error: {
3 code: string
4 message: string
5 details?: Record<string, any>
6 timestamp: string
7 path: string
8 }
9}
10
11// グローバルエラーハンドラー
12app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
13 const statusCode = err instanceof HttpError ? err.statusCode : 500
14
15 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 }
24
25 // ログ記録
26 logger.error({
27 ...errorResponse,
28 stack: err.stack,
29 })
30
31 res.status(statusCode).json(errorResponse)
32})
33
34// カスタムエラークラス
35class ValidationError extends Error {
36 constructor(public details: Record<string, string>) {
37 super('Validation failed')
38 this.name = 'VALIDATION_ERROR'
39 }
40}
41
42class 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'
2
3// 基本的なレート制限
4const limiter = rateLimit({
5 windowMs: 15 * 60 * 1000, // 15分
6 max: 100, // 最大100リクエスト
7 message: 'リクエストが多すぎます。後でもう一度お試しください。',
8 standardHeaders: true,
9 legacyHeaders: false,
10})
11
12app.use('/api/', limiter)
13
14// エンドポイント固有のレート制限
15const strictLimiter = rateLimit({
16 windowMs: 60 * 60 * 1000, // 1時間
17 max: 10, // 最大10リクエスト
18})
19
20app.post('/api/auth/login', strictLimiter, loginHandler)
21
22// ユーザーベースのレート制限
23const userLimiter = rateLimit({
24 keyGenerator: (req) => req.user?.id || req.ip,
25 max: 1000,
26})

##キャッシング戦略

パフォーマンスを向上させるための効果的なキャッシング。

typescript
1import { Redis } from 'ioredis'
2
3const redis = new Redis()
4
5// キャッシュミドルウェア
6function cacheMiddleware(duration: number) {
7 return async (req: Request, res: Response, next: NextFunction) => {
8 const key = `cache:${req.path}:${JSON.stringify(req.query)}`
9
10 const cached = await redis.get(key)
11 if (cached) {
12 return res.json(JSON.parse(cached))
13 }
14
15 const originalJson = res.json.bind(res)
16 res.json = (data: any) => {
17 redis.setex(key, duration, JSON.stringify(data))
18 return originalJson(data)
19 }
20
21 next()
22 }
23}
24
25// 使用例
26app.get('/api/products', cacheMiddleware(300), getProducts)
27
28// ETags によるキャッシング
29app.get('/api/users/:id', async (req, res) => {
30 const user = await User.findById(req.params.id)
31 const etag = generateEtag(user)
32
33 if (req.headers['if-none-match'] === etag) {
34 return res.status(304).end()
35 }
36
37 res.setHeader('ETag', etag)
38 res.setHeader('Cache-Control', 'max-age=300')
39 res.json(user)
40})

##認証と認可

JWT を使用した安全な認証システム。

typescript
1import jwt from 'jsonwebtoken'
2
3interface JWTPayload {
4 userId: string
5 role: string
6 permissions: string[]
7}
8
9// トークン生成
10function generateToken(user: User): string {
11 const payload: JWTPayload = {
12 userId: user.id,
13 role: user.role,
14 permissions: user.permissions,
15 }
16
17 return jwt.sign(payload, process.env.JWT_SECRET!, {
18 expiresIn: '24h',
19 issuer: 'cyberwolf.studio',
20 })
21}
22
23// 認証ミドルウェア
24function authenticate(req: Request, res: Response, next: NextFunction) {
25 const token = req.headers.authorization?.replace('Bearer ', '')
26
27 if (!token) {
28 return res.status(401).json({ error: 'トークンが必要です' })
29 }
30
31 try {
32 const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
33 req.user = decoded
34 next()
35 } catch (error) {
36 return res.status(401).json({ error: '無効なトークンです' })
37 }
38}
39
40// 権限チェック
41function authorize(...permissions: string[]) {
42 return (req: Request, res: Response, next: NextFunction) => {
43 if (!req.user) {
44 return res.status(401).json({ error: '認証が必要です' })
45 }
46
47 const hasPermission = permissions.some(p =>
48 req.user!.permissions.includes(p)
49 )
50
51 if (!hasPermission) {
52 return res.status(403).json({ error: 'アクセスが拒否されました' })
53 }
54
55 next()
56 }
57}
58
59// 使用例
60app.delete(
61 '/api/users/:id',
62 authenticate,
63 authorize('users:delete'),
64 deleteUser
65)

##リクエスト検証

入力データを検証してセキュリティを強化します。

typescript
1import { z } from 'zod'
2
3// スキーマ定義
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})
14
15// 検証ミドルウェア
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}
39
40// 使用例
41app.post('/api/users', validate(createUserSchema), createUser)

##APIドキュメンテーション

OpenAPI/Swagger を使用した自動ドキュメント生成。

typescript
1import swaggerJsdoc from 'swagger-jsdoc'
2import swaggerUi from 'swagger-ui-express'
3
4const 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}
21
22const specs = swaggerJsdoc(options)
23app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs))
24
25/**
26 * @swagger
27 * /api/users:
28 * get:
29 * summary: ユーザー一覧を取得
30 * tags: [Users]
31 * parameters:
32 * - in: query
33 * name: page
34 * schema:
35 * type: integer
36 * description: ページ番号
37 * responses:
38 * 200:
39 * description: 成功
40 */

##結論

優れたAPI設計は、一貫性、セキュリティ、パフォーマンス、開発者エクスペリエンスのバランスです。これらのパターンに従うことで、スケーラブルで保守しやすいAPIを構築できます。

***

API設計のコンサルティングが必要ですか?お問い合わせください。

APIRESTアーキテクチャバックエンド