📐 実装規約

📅 最終更新: 2026-05-13
🔗 関連: design.md (index) / requirements.md / sitemap.md / architecture.md / operations.md

⚡ 1. Server Action 戻り値 / エラー伝達

すべての Server Action は ActionResult<T> を return する。throw は「想定外(DB ダウン等)」のみで、それは error.tsx が受ける。

flowchart TD Submit["📝 フォーム submit"]:::input Action["⚡ Server Action 呼び出し"]:::step Guard{"🔐 requireSession()"}:::guard Validate{"🛡️ Zod safeParse"}:::guard DB["🗄️ DB 操作 + revalidatePath"]:::step OK["✅ ok: data"]:::ok Err1["❌ err: UNAUTHORIZED"]:::err Err2["❌ err: VALIDATION
+ fieldErrors"]:::err Client["🚀 router.push(...)"]:::client ClientErr["📛 applyActionErrors
→ setError"]:::client Submit --> Action --> Guard Guard -->|未認証| Err1 Guard -->|OK| Validate Validate -->|失敗| Err2 Validate -->|成功| DB --> OK --> Client Err1 --> ClientErr Err2 --> ClientErr classDef input fill:#eef3f8,stroke:#6b9bc7,color:#1a2533 classDef step fill:#f0f6fc,stroke:#1e5a8e,color:#1a2533 classDef guard fill:#fff3e6,stroke:#c47a14,color:#1a2533 classDef ok fill:#e8f3ec,stroke:#2f7a4a,color:#1a2533 classDef err fill:#fbe9e9,stroke:#b13838,color:#1a2533 classDef client fill:#eef3f8,stroke:#6b9bc7,color:#1a2533
shared/lib/action.ts
export type ErrorCode =
  | 'VALIDATION'    // Zod 失敗、fieldErrors 同梱
  | 'UNAUTHORIZED'  // 未ログイン or 権限なし
  | 'NOT_FOUND'     // 対象リソースが他人 or 存在しない
  | 'RATE_LIMITED'  // Upstash で弾かれた
  | 'CONFLICT'      // UNIQUE 違反など
  | 'INTERNAL';     // 想定外(基本これに落ちる前に throw)

export type ActionError = {
  code: ErrorCode;
  message: string;                       // UI 言語は日本語のみ
  fieldErrors?: Record<string, string>;  // RHF setError に流す
};

export type ActionResult<T> =
  | { ok: true;  data: T }
  | { ok: false; error: ActionError };

export const okResult  = <T>(data: T): ActionResult<T> => ({ ok: true, data });
export const errResult = (
  code: ErrorCode,
  message: string,
  fieldErrors?: Record<string, string>,
): ActionResult<never> => ({ ok: false, error: { code, message, fieldErrors } });

📋 規約


🔐 2. 認証ガード実装(requireSession)

責務分担は architecture.md §4 を参照。ここでは実装ヘルパのみ。

shared/lib/auth-guard.ts
import { auth } from '@/modules/auth';
import { headers } from 'next/headers';
import { errResult, okResult, type ActionResult } from './action';

type AuthedSession = NonNullable<Awaited<ReturnType<typeof auth.api.getSession>>>;

export async function requireSession(): Promise<ActionResult<AuthedSession>> {
  const session = await auth.api.getSession({ headers: await headers() });
  if (!session) return errResult('UNAUTHORIZED', '認証が必要です');
  return okResult(session);
}

各 Action での使い方:

'use server';
export async function createBean(input: BeanInput): Promise<ActionResult<{ id: string }>> {
  const auth = await requireSession();
  if (!auth.ok) return auth;

  const parsed = createBeanSchema.safeParse(input);
  if (!parsed.success) return errResult('VALIDATION', '入力エラー', flattenZodErrors(parsed.error));

  // ... DB INSERT、revalidatePath('/beans') 等
  return okResult({ id: newId });
}

📝 3. フォーム実装

shared/lib/form.ts
import type { UseFormSetError, FieldValues, Path } from 'react-hook-form';
import type { ActionError, ActionResult } from './action';

export function applyActionErrors<T, F extends FieldValues>(
  result: ActionResult<T>,
  setError: UseFormSetError<F>,
): result is { ok: false; error: ActionError } {
  if (result.ok) return false;
  const { code, message, fieldErrors } = result.error;
  if (code === 'VALIDATION' && fieldErrors) {
    for (const [field, msg] of Object.entries(fieldErrors)) {
      setError(field as Path<F>, { message: msg });
    }
  } else {
    setError('root', { message });
  }
  return true;
}
modules/bean/components/bean-form.tsx
'use client';
export function BeanForm() {
  const router = useRouter();
  const { register, handleSubmit, setError, formState: { errors, isSubmitting } } = useForm<BeanInput>({
    resolver: zodResolver(createBeanSchema),
  });

  const onSubmit = handleSubmit(async (data) => {
    const result = await createBean(data);
    if (applyActionErrors(result, setError)) return;
    router.push(`/beans/${result.data.id}`);
  });

  return (
    <form onSubmit={onSubmit}>
      <Input {...register('name')} errorMessage={errors.name?.message} />
      {/* ... */}
    </form>
  );
}

📄 4. ページング / 絞り込み

一覧方式
Bean 一覧 ページングなし、全件取得・created_at DESCarchived 表示切替トグルのみ
Brew 一覧 offset ベース(?page=N&pageSize=20)+ ★評価 / 日付範囲 / Bean フィルタ

🔑 5. 主キー / URL ID