API 設計
API Design
Next.js App Router(OpenNext / Cloudflare Workers 上)で提供する REST API の仕様。
1. エンドポイント一覧
Endpoint List
| メソッド | パス | 認証 | 説明 |
|---|---|---|---|
| GET | /api/terms | 不要 | 用語一覧取得・検索・カテゴリ絞込 |
| POST | /api/terms | 🔒 管理者 | 用語新規登録 |
| PUT | /api/terms/[id] | 🔒 管理者 | 用語更新 |
| DELETE | /api/terms/[id] | 🔒 管理者 | 用語削除(R2 画像も削除) |
| POST | /api/terms/upload-image | 🔒 管理者 | 説明画像アップロード → { key } |
| GET | /api/media/[...path] | 不要 | R2 保存画像のプロキシ配信 |
| POST | /api/auth/login | 不要 | 管理者ログイン(HMAC セッション Cookie 発行) |
| POST | /api/auth/logout | 不要 | 管理者ログアウト(Cookie クリア) |
| GET | /api/auth/session | 不要 | セッション確認 → { authenticated: boolean } |
🔒 管理者必須: requireAdminSession ミドルウェアで保護。未認証の場合は 401 を返す。
2. 用語一覧取得・検索
Term List Retrieval & Search
GET
/api/terms
認証不要
用語の一覧取得・キーワード検索・カテゴリ絞込・ページング
Query Parameters
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
q | string | "" | 検索キーワード |
searchBy | "name" | "description" | "name" | 検索対象フィールド |
category | "" | Category | "" | カテゴリ絞込(空文字=すべて) |
page | number | 1 | ページ番号(1 始まり) |
limit | number | 20 | 1 ページあたりの件数(最大 100) |
Request Examples
GET /api/terms?q=ec2&searchBy=name&page=1&limit=20
GET /api/terms?q=仮想&searchBy=description&category=Compute
GET /api/terms?category=Security
GET /api/terms
Responses
200 OK
{
"terms": [
{
"id": 1,
"name": "Amazon EC2",
"abbreviation": "EC2",
"category": "Compute",
"description": "仮想サーバーを提供するコンピューティングサービス",
"descriptionImageKey": "terms/original/uuid.png",
"descriptionImageSlimKey": "terms/slim/uuid.webp",
"descriptionImageUrl": "/api/media/terms/slim/uuid.webp",
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-01-15T10:00:00.000Z"
}
],
"pagination": {
"total": 60,
"page": 1,
"limit": 20,
"totalPages": 3
}
}
descriptionImageUrl: slim キーがある場合は/api/media/<slimKey>、ない場合は/api/media/<originalKey>、画像なしの場合はnull。
400 Bad Request
{ "error": "Invalid query parameter", "details": [...] }
500 Internal Server Error
{ "error": "Internal server error" }
3. 用語新規登録
Term Registration
POST
/api/terms
🔒 管理者必須
新しい用語を登録する。同名の用語は 409 を返す。
Request Body application/json
{
"name": "Amazon EC2",
"abbreviation": "EC2",
"category": "Compute",
"description": "仮想サーバーを提供するサービス",
"descriptionImageKey": "terms/original/uuid.png"
}
| フィールド | 型 | 必須 | バリデーション |
|---|---|---|---|
name | string | 必須 | 1〜200 文字、空白のみ不可 |
abbreviation | string | null | - | 最大 50 文字 |
category | Category | null | - | CATEGORIES 定数のいずれかの値 |
description | string | 必須 | 1〜2000 文字、空白のみ不可 |
descriptionImageKey | string | null | - | terms/original/ または terms/uuid.* 形式の R2 キー |
Responses
201 Created
{
"term": {
"id": 61,
"name": "Amazon EC2",
"abbreviation": "EC2",
"category": "Compute",
"description": "仮想サーバーを提供するサービス",
"descriptionImageKey": "terms/original/uuid.png",
"descriptionImageSlimKey": null,
"descriptionImageUrl": "/api/media/terms/original/uuid.png",
"createdAt": "2024-04-18T10:00:00.000Z",
"updatedAt": "2024-04-18T10:00:00.000Z"
}
}
400 Bad Request
{ "error": "Validation failed", "details": [{ "field": "name", "message": "必須項目です" }] }
401 Unauthorized
{ "error": "Unauthorized" }
409 Conflict
{ "error": "Term with this name already exists" }
4. 用語更新
Term Update
PUT
/api/terms/[id]
🔒 管理者必須
指定 ID の用語を更新する。
descriptionImageKey を null で送ると画像削除。
URL Parameter
| パラメータ | 型 | 説明 |
|---|---|---|
id | number | 更新対象の用語 ID |
Request Body application/json
{
"name": "Amazon EC2",
"abbreviation": "EC2",
"category": "Compute",
"description": "更新後の説明文",
"descriptionImageKey": "terms/original/new-uuid.png"
}
descriptionImageKeyを省略した場合は既存の画像キーを維持。画像を削除する場合はnullを送信する。
Responses
200 OK
{ "term": { ...updatedTerm } }
400 Bad Request
{ "error": "Invalid id" }
401 Unauthorized
{ "error": "Unauthorized" }
404 Not Found
{ "error": "Term not found" }
409 Conflict
{ "error": "Term with this name already exists" }
5. 用語削除
Term Deletion
DELETE
/api/terms/[id]
🔒 管理者必須
用語を削除し、紐付く R2 画像(original / slim)も同時に削除する。
処理フロー
- D1 から用語を削除する
descriptionImageKeyまたはdescriptionImageSlimKeyが存在する場合、対応する R2 オブジェクトも削除する
Responses
200 OK
{ "message": "Term deleted successfully" }
400 Bad Request
{ "error": "Invalid id" }
401 Unauthorized
{ "error": "Unauthorized" }
404 Not Found
{ "error": "Term not found" }
6. 説明画像アップロード
Description Image Upload
POST
/api/terms/upload-image
🔒 管理者必須
画像を R2 に保存し、最適化キューにメッセージを送信する。
Request Body multipart/form-data
| フィールド | 型 | 必須 | バリデーション |
|---|---|---|---|
file | File | 必須 | JPEG / PNG / GIF / WebP、5MB 以下 |
処理フロー
- MIME タイプ・ファイルサイズのバリデーション
- R2 に
terms/original/<uuid>.<ext>として保存 IMAGE_OPTIMIZE_QUEUEにメッセージを送信(軽量版生成トリガー)
Responses
200 OK
{ "key": "terms/original/uuid.png" }
400 Bad Request
{ "error": "file が必要です" }
{ "error": "対応形式は JPEG / PNG / GIF / WebP のみです" }
{ "error": "ファイルサイズは 5MB 以下にしてください" }
401 Unauthorized
{ "error": "Unauthorized" }
7. R2 画像プロキシ配信
R2 Image Proxy Delivery
GET
/api/media/[...path]
認証不要
R2 に保存された画像を
Content-Type 付きでプロキシ配信する。
Request Examples
GET /api/media/terms/original/uuid.png
GET /api/media/terms/slim/uuid.webp
Responses
200 OK
画像バイナリ(Content-Type: image/png 等)
403 Forbidden
{ "error": "Forbidden" }
パストラバーサル検出(.. を含むパス)
404 Not Found
{ "error": "Not found" }
8. 管理者ログイン
Admin Login
POST
/api/auth/login
認証不要
HMAC-SHA256 署名付きセッション Cookie を発行する。有効期限 7 日。
Request Body application/json
{ "email": "admin@example.com", "password": "secret" }
| フィールド | 型 | バリデーション |
|---|---|---|
email | string | 必須・空文字不可 |
password | string | 必須・空文字不可 |
処理フロー
email/passwordが空の場合は 400 を返すADMIN_EMAIL/ADMIN_PASSWORD環境変数と照合(タイミング安全比較)- 不一致の場合は 401 を返す
- 一致した場合は HMAC-SHA256 署名付きトークンを生成し
admin_sessionCookie にセット
Responses
200 OK
{ "ok": true }
Set-Cookie: admin_session=<token>; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800
本番環境では末尾に; Secureが付与される。
400 Bad Request
{ "error": "Invalid request" }
401 Unauthorized
{ "error": "Unauthorized" }
9. 管理者ログアウト
Admin Logout
POST
/api/auth/logout
認証不要
セッション Cookie を Max-Age=0 でクリアする。
Responses
200 OK
{ "ok": true }
Set-Cookie: admin_session=; Max-Age=0; ...(Cookie をクリア)
10. セッション確認
Session Verification
GET
/api/auth/session
認証不要
Cookie を検証してログイン状態を返す。フロントエンドの初期化時に使用。
Responses
200 OK
{ "authenticated": true }
{ "authenticated": false }
11. バリデーションスキーマ(Zod)
Validation Schema (Zod)
// src/features/terms/schemas/term.ts
import { z } from "zod"
import { CATEGORIES } from "@/features/terms/constants/categories"
import { isValidDescriptionImageKey } from "@/lib/descriptionImage"
const descriptionImageKeyField = z
.union([
z.string().max(512).refine((s) => isValidDescriptionImageKey(s), "画像キーの形式が不正です"),
z.null(),
])
.optional()
export const termSchema = z.object({
name: z.string().min(1, "用語名は必須です").max(200).trim(),
abbreviation: z.string().max(50).optional().nullable(),
category: z.enum(CATEGORIES).optional().nullable(),
description: z.string().min(1, "説明は必須です").max(2000).trim(),
descriptionImageKey: descriptionImageKeyField,
})
// src/features/terms/schemas/searchParams.ts
export const searchParamsSchema = z.object({
q: z.string().default(""),
searchBy: z.enum(["name", "description"]).default("name"),
category: z.union([z.literal(""), z.enum(CATEGORIES)]).default(""),
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
})
12. エラーハンドリング方針
Error Handling Policy
| HTTP ステータス | 意味 | 使用場面 |
|---|---|---|
| 200 | 成功 | GET, PUT, DELETE 成功時 |
| 201 | 作成成功 | POST 成功時 |
| 400 | リクエスト不正 | バリデーションエラー・不正な ID |
| 401 | 未認証 | 管理者セッションなしで保護エンドポイントにアクセス |
| 403 | アクセス禁止 | パストラバーサル等 |
| 404 | リソース未存在 | 存在しない ID / R2 キーへのアクセス |
| 409 | 重複 | 同名用語の登録 |
| 500 | サーバーエラー | 予期しないエラー |