パイプライン設計
🔄 パイプライン設計:データの型管理と画面接続のアーキテクチャ
Section titled “🔄 パイプライン設計:データの型管理と画面接続のアーキテクチャ”パイプライン設計の詳細については、設計ガイドのパイプライン設計を参照してください。
このドキュメントでは、Next.js(App Router)特有の実装例を中心に説明します。
Next.jsでのパイプライン設計の実装
Section titled “Next.jsでのパイプライン設計の実装”Next.jsでは、Server ComponentsとServer Actionsを活用して、パイプライン設計を実装できます。
Next.jsでの実装例
Section titled “Next.jsでの実装例”Schema層、DTO層、Form層の実装
Section titled “Schema層、DTO層、Form層の実装”各層の詳細な実装については、設計ガイドのパイプライン設計を参照してください。
Page層: 司令塔としてデータを取得し、Formへ配分
Section titled “Page層: 司令塔としてデータを取得し、Formへ配分”役割: サーバーコンポーネント(Page)でデータを取得し、適切な層を経由してフォームに渡す。
実装例(App Router)
Section titled “実装例(App Router)”import { fetchUser } from '@/api/schema/userSchema';import { userApiToDto } from '@/types/user';import { userDtoToFormInput } from '@/components/UserForm/schema';import UserForm from '@/components/UserForm/UserForm';import { updateUserAction } from './actions';
type EditUserPageProps = { params: Promise<{ id: string; }>;};
export default async function EditUserPage({ params }: EditUserPageProps) { // Next.js 15以降: paramsをawaitする必要がある const { id } = await params;
// 1. API層: unknown -> Strict Schema const apiUser = await fetchUser(Number(id));
// 2. DTO層: API型 -> UI型 const user = userApiToDto(apiUser);
// 3. Form層: DTO型 -> Form型 const formInitialValues = userDtoToFormInput(user);
// 4. フォームに渡す return ( <div> <h1>ユーザー編集</h1> <UserForm initialValues={formInitialValues} userId={Number(id)} onSubmit={updateUserAction} /> </div> );}Server Actionでの実装
Section titled “Server Actionでの実装”'use server';
import { userFormInputToApiRequest } from '@/components/UserForm/schema';import { createUserApiSchema } from '@/components/UserForm/schema';import { userApiSchema } from '@/api/schema/userSchema';import { revalidatePath } from 'next/cache';
// useActionState用の型定義type ActionState = { errors?: { firstName?: string; lastName?: string; email?: string; general?: string; }; success?: boolean;};
export async function updateUserAction( prevState: ActionState | null, formData: FormData): Promise<ActionState> { // 1. FormDataから値を取得 const firstName = formData.get('firstName') as string; const lastName = formData.get('lastName') as string; const email = formData.get('email') as string; const userId = Number(formData.get('userId'));
// 2. フォームデータをAPIリクエスト形式に変換 const apiRequest = userFormInputToApiRequest({ firstName, lastName, email, });
// 3. ✅ 重要: APIに送るデータもZodでパース(洗浄)してから送信 // これにより、DTOからフォームに変換した際のゴミデータがAPIに飛ぶのを防ぐ const parseResult = createUserApiSchema.safeParse(apiRequest);
if (!parseResult.success) { // バリデーションエラーを返す const errors: ActionState['errors'] = {}; parseResult.error.errors.forEach((error) => { const field = error.path[0] as string; if (field === 'first_name') errors.firstName = error.message; else if (field === 'last_name') errors.lastName = error.message; else if (field === 'email') errors.email = error.message; }); return { errors }; }
const validatedRequest = parseResult.data;
try { // 4. APIを呼び出す(洗浄済みのデータを送信) const response = await fetch(`https://api.example.com/users/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(validatedRequest), // ✅ 洗浄済みデータ });
if (!response.ok) { const errorData = await response.json().catch(() => ({})); return { errors: { general: errorData.message || 'ユーザーの更新に失敗しました', }, }; }
const data: unknown = await response.json();
// 5. ✅ レスポンスもZodでパース(洗浄) const validatedResponse = userApiSchema.parse(data);
// 6. キャッシュを無効化 revalidatePath(`/users/${userId}/edit`);
return { success: true }; } catch (error) { return { errors: { general: error instanceof Error ? error.message : '予期しないエラーが発生しました', }, }; }}Client Componentでの実装(useActionState使用)
Section titled “Client Componentでの実装(useActionState使用)”'use client';
import { useActionState } from 'react';import { updateUserAction } from '@/app/users/[id]/edit/actions';
type UserFormProps = { initialValues: { firstName: string; lastName: string; email: string; }; userId: number;};
export default function UserForm({ initialValues, userId,}: UserFormProps) { // useActionStateでServer Actionの状態を管理 // サーバーからのバリデーションエラー(「メールアドレスが既に使われています」など)をUIに返す const [state, action, isPending] = useActionState(updateUserAction, null);
return ( <form action={action}> {/* userIdをhidden inputで送信 */} <input type="hidden" name="userId" value={userId} />
<div> <label htmlFor="firstName">名前</label> <input id="firstName" name="firstName" defaultValue={initialValues.firstName} aria-invalid={state?.errors?.firstName ? 'true' : 'false'} /> {state?.errors?.firstName && ( <span role="alert" className="error"> {state.errors.firstName} </span> )} </div>
<div> <label htmlFor="lastName">姓</label> <input id="lastName" name="lastName" defaultValue={initialValues.lastName} aria-invalid={state?.errors?.lastName ? 'true' : 'false'} /> {state?.errors?.lastName && ( <span role="alert" className="error"> {state.errors.lastName} </span> )} </div>
<div> <label htmlFor="email">メールアドレス</label> <input id="email" type="email" name="email" defaultValue={initialValues.email} aria-invalid={state?.errors?.email ? 'true' : 'false'} /> {state?.errors?.email && ( <span role="alert" className="error"> {state.errors.email} </span> )} </div>
{state?.errors?.general && ( <div role="alert" className="error"> {state.errors.general} </div> )}
{state?.success && ( <div className="success">ユーザー情報を更新しました</div> )}
<button type="submit" disabled={isPending}> {isPending ? '送信中...' : '送信'} </button> </form> );}React Hook FormとuseActionStateの組み合わせ(オプション)
Section titled “React Hook FormとuseActionStateの組み合わせ(オプション)”React Hook Formを使いたい場合でも、useActionStateと組み合わせることができます:
'use client';
import { useActionState } from 'react';import { useForm } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';import { userFormSchema, type UserFormInput } from './schema';import { updateUserAction } from '@/app/users/[id]/edit/actions';
type UserFormProps = { initialValues: UserFormInput; userId: number;};
export default function UserFormWithRHF({ initialValues, userId,}: UserFormProps) { const [state, formAction, isPending] = useActionState(updateUserAction, null);
const { register, handleSubmit, formState: { errors: clientErrors }, } = useForm<UserFormInput>({ resolver: zodResolver(userFormSchema), defaultValues: initialValues, });
const onSubmit = handleSubmit((data) => { // FormDataを作成してServer Actionに送信 const formData = new FormData(); formData.append('userId', userId.toString()); formData.append('firstName', data.firstName); formData.append('lastName', data.lastName); formData.append('email', data.email); formAction(formData); });
return ( <form onSubmit={onSubmit}> <div> <label htmlFor="firstName">名前</label> <input id="firstName" {...register('firstName')} /> {clientErrors.firstName && ( <span role="alert">{clientErrors.firstName.message}</span> )} {state?.errors?.firstName && ( <span role="alert">{state.errors.firstName}</span> )} </div>
<div> <label htmlFor="lastName">姓</label> <input id="lastName" {...register('lastName')} /> {clientErrors.lastName && ( <span role="alert">{clientErrors.lastName.message}</span> )} {state?.errors?.lastName && ( <span role="alert">{state.errors.lastName}</span> )} </div>
<div> <label htmlFor="email">メールアドレス</label> <input id="email" type="email" {...register('email')} /> {clientErrors.email && ( <span role="alert">{clientErrors.email.message}</span> )} {state?.errors?.email && ( <span role="alert">{state.errors.email}</span> )} </div>
{state?.errors?.general && ( <div role="alert">{state.errors.general}</div> )}
{state?.success && <div>ユーザー情報を更新しました</div>}
<button type="submit" disabled={isPending}> {isPending ? '送信中...' : '送信'} </button> </form> );}パイプライン設計の詳細な説明、データフローの全体像、各層の責務、ベストプラクティスについては、設計ガイドのパイプライン設計を参照してください。
Reactでの実装例については、Reactガイドのパイプライン設計を参照してください。
Next.js(App Router)でのポイント
Section titled “Next.js(App Router)でのポイント”- Next.js 15のparamsはPromise:
paramsはNext.js 15以降ではPromise型で、await paramsが必要です。const { id } = await params;のように必ずawaitしてください。 - useActionStateの活用: サーバーからのバリデーションエラー(「メールアドレスが既に使われています」など)をUIに返すために、React公式の
useActionState(旧useFormState)を使用します。Server Actionはエラーをthrowするのではなく、エラーオブジェクトを返すように実装します。 - 「逆方向」の洗浄: APIからデータをもらう時は
zod.parseしていますが、APIにデータを送る直前にもzod.parse(またはsafeParse)で洗浄します。これにより、DTOからフォームに変換した際のゴミデータがAPIに飛ぶのを防げます。 - Server Componentsの活用: サーバー側でデータを取得し、型変換を行う
- Server Actionsの活用: フォーム送信をServer Actionで処理し、型安全性を保つ(
'use server'は関数の先頭に記述) - Client Componentsとの分離: フォームなどのインタラクティブな部分は
'use client'でClient Componentとして実装 - MSWとの連携: 開発環境ではMSW(モックサービスワーカー)を使用してAPIをモック
このパイプライン設計を守ることで、APIとUIの責務が明確に分離され、保守性の高いNext.jsアプリケーションを構築できます。