Skip to content

パイプライン設計

🔄 パイプライン設計:データの型管理と画面接続のアーキテクチャ

Section titled “🔄 パイプライン設計:データの型管理と画面接続のアーキテクチャ”

パイプライン設計の詳細については、設計ガイドのパイプライン設計を参照してください。

このドキュメントでは、Next.js(App Router)特有の実装例を中心に説明します。

Next.jsでのパイプライン設計の実装

Section titled “Next.jsでのパイプライン設計の実装”

Next.jsでは、Server ComponentsとServer Actionsを活用して、パイプライン設計を実装できます。


各層の詳細な実装については、設計ガイドのパイプライン設計を参照してください。

Page層: 司令塔としてデータを取得し、Formへ配分

Section titled “Page層: 司令塔としてデータを取得し、Formへ配分”

役割: サーバーコンポーネント(Page)でデータを取得し、適切な層を経由してフォームに渡す。

app/users/[id]/edit/page.tsx
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>
);
}
app/users/[id]/edit/actions.ts
'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使用)”
components/UserForm/UserForm.tsx
'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と組み合わせることができます:

components/UserForm/UserFormWithRHF.tsx
'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ガイドのパイプライン設計を参照してください。

  1. Next.js 15のparamsはPromise: paramsはNext.js 15以降ではPromise型で、await paramsが必要です。const { id } = await params;のように必ずawaitしてください。
  2. useActionStateの活用: サーバーからのバリデーションエラー(「メールアドレスが既に使われています」など)をUIに返すために、React公式のuseActionState(旧useFormState)を使用します。Server Actionはエラーをthrowするのではなく、エラーオブジェクトを返すように実装します。
  3. 「逆方向」の洗浄: APIからデータをもらう時はzod.parseしていますが、APIにデータを送る直前にもzod.parse(またはsafeParse)で洗浄します。これにより、DTOからフォームに変換した際のゴミデータがAPIに飛ぶのを防げます。
  4. Server Componentsの活用: サーバー側でデータを取得し、型変換を行う
  5. Server Actionsの活用: フォーム送信をServer Actionで処理し、型安全性を保つ('use server'は関数の先頭に記述)
  6. Client Componentsとの分離: フォームなどのインタラクティブな部分は'use client'でClient Componentとして実装
  7. MSWとの連携: 開発環境ではMSW(モックサービスワーカー)を使用してAPIをモック

このパイプライン設計を守ることで、APIとUIの責務が明確に分離され、保守性の高いNext.jsアプリケーションを構築できます。