見出し画像

Server Actions の 「境界」 を先に決める。 バリデーション・認可・エラーの最小設計

「どこでやるか」 が決まっていないと、迷う

Next.js の Server Actions でフォームや mutation を書くとき、こんな迷いが出てきませんか?

「バリデーションは、クライアントで先にやる? サーバーだけでいい?」
「認可チェックは、Server Action の最初? それともミドルウェア?」
「エラーは throw する? それとも return { error } で返す?」

「境界」——つまり「どこで何をするか」——が決まっていないと、Action を追加するたびに「前回はどう書いたっけ」と揺れてしまいます。
僕も、最初の頃は Action ごとにバラバラな書き方になってしまい、あとから読み返すと自分でもわけがわからなくなることがありました。

実装を書き始める前に、「境界」を決めておく。

これだけで、Server Actions の設計が驚くほどシンプルで一貫します。
今回は、個人開発や小規模プロダクトで十分な「バリデーション・認可・エラーの最小設計」を、境界の決め方と一緒に共有します。


「境界」 とは何か

境界とは、「どのレイヤーで、何を担当するか」の線引きです。

バリデーション:
入力が「正しい形か」をどこでチェックするか。
(クライアント / Server Action の入口 / 両方)

認可:
「このユーザーがこの操作をしていいか」をどこでチェックするか。
(ミドルウェア / Server Action の先頭 / サービス層)

エラー:
失敗したとき、呼び出し元にどう返すか。
(throw / return { error } / ステータスコード)

この 3 つを「うちのプロジェクトではこうする」と先に決めておくと、迷いが消えます。
「最小設計」なので、大規模向けの複雑なレイヤー分けはしません。
個人開発で困らないラインだけに絞ります。

ここからは、僕が実際に採用している「3 つの境界」の決め方と、その方針に沿った最小のコード例を公開します。
Next.js の Server Actions の公式ドキュメントuseActionState を前提にした、そのまま真似できる設計です。

【境界 1】 バリデーションは 「Server Action の入口」 でやる

決め方:
バリデーションは Server Action の先頭で必ずやる

クライアント側のチェックは「あれば UX が良い」程度で、信頼の境界はサーバーに置く。

なぜ:
クライアントは改ざんできるので、サーバーで必ず検証しないと不正なデータが流れる。

一方で、クライアントでも同じスキーマ(Zod)で先行チェックすれば、送信前にエラーを出せるので UX は良くなる。

どうやるか:
Zod でスキーマを 1 つ定義し、Server Action の最初で `schema.safeParse(formData)` する。

失敗したら `return { error: '...' }` で返す(後述のエラー境界に合わせる)。

👇️コードサンプル
// 例: Server Action の入口で Zod で検証
import { z } from 'zod';

const inputSchema = z.object({
  name: z.string().min(1, '名前を入力してください'),
  email: z.string().email('正しいメールアドレスを入力してください'),
});

export async function submitContact(formData: FormData) {
  const parsed = inputSchema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) {
    const msg = parsed.error.flatten().formErrors[0] ?? '入力に誤りがあります';
    return { error: msg };
  }
  // 以降、parsed.data は型安全に利用できる
  // ...
}

境界の一言:
「バリデーションは Server Action の入口で Zod。
 クライアントは任意で同じスキーマを使う。」

【境界 2】 認可は 「Server Action の先頭」 でやる

決め方:
認可(このユーザーがこの操作をしていいか)はServer Action の最初で必ずチェックする

ミドルウェアでルート単位のガードはしても、「この Action を実行していいか」は Action 内で明示的に書く

なぜ:
Server Action は URL がなく、ミドルウェアだけだと「どの Action か」に応じた細かい認可が書きにくい。

Action の先頭で `getServerSession()` や `cookies()` を見て、未認可なら `return { error: '...' }` にすると、読み手にも「ここで認可している」が一目でわかる。

どうやるか:
Action の 1 行目付近でセッション or ユーザーを取得し、なければ即 `return { error: 'ログインしてください' }`。

DB や外部 API を叩く前にやる。

👇️コードサンプル
// 例: Action の先頭で認可
export async function updateProfile(formData: FormData) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) {
    return { error: 'ログインしてください' };
  }
  // 以降、session.user.id を使って更新処理
  // ...
}

境界の一言:
「認可は Server Action の先頭。
 未認可なら return { error } で終了。」

【境界 3】 エラーは 「throw ではなく return { error }」 で返す

決め方:
フォームや UI から呼ばれる Server Action では、throw ではなく `return { error: string }` で返す

成功時は `return { success: true }` や必要なデータを返す。
こうすると useActionState で受け取った `formState` から `error` を読んで表示できる。

なぜ:
throw だとクライアント側で try/catch や error boundary に依存しがち。

`return { error }` に統一すると、「成功か失敗か」が戻り値の型で明確になり、フォームのエラー表示と相性が良い。

どうやるか:
バリデーション失敗・認可失敗・ビジネスロジックの失敗すべて、`return { error: 'メッセージ' }`。

成功時は `return { success: true, data: ... }` など。
型は `{ error?: string; success?: boolean; data?: ... }` のように 1 本化する。

👇️コードサンプル
// 例: 戻り値の型を統一
type ActionResult = { error: string } | { success: true; id: string };

export async function createPost(formData: FormData): Promise<ActionResult> {
  const session = await getServerSession(authOptions);
  if (!session?.user?.id) return { error: 'ログインしてください' };

  const parsed = postSchema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) {
    const msg = parsed.error.flatten().formErrors[0] ?? '入力に誤りがあります';
    return { error: msg };
  }

  try {
    const post = await db.post.create({ data: { ...parsed.data, userId: session.user.id } });
    return { success: true, id: post.id };
  } catch (e) {
    return { error: '保存に失敗しました' };
  }
}

境界の一言:
「エラーは throw しない。
 return { error } で返し、useActionState で表示する。」

最小設計のまとめ (3 つの境界)

👇️Markdown形式
| 項目         | 境界の決め方                                       |
| :----------- | :------------------------------------------------- |
| バリデーション | Server Action の入口で Zod。クライアントは任意。   |
| 認可         | Server Action の先頭でセッション確認。未認可なら return { error }。 |
| エラー       | throw しない。return { error } で返す。useActionState で受け取る。 |

この 3 つを「プロジェクトの境界」として決めておくと、新しい Server Action を足すたびに迷わず、一貫したコードが積み上がります。
まずは境界を決めてから、実装を書く。
その順番を守るだけで、設計がぐっと軽く、読みやすくなります。

あわせて読みたい

この記事を読んだあなたにおすすめの、Next.js と Server Actions まわりの記事です。

▼ フォーム実装の具体的なテンプレートが欲しいときに

境界を決めたあと、実際のコードを「定義層・処理層・表示層」でどう書くか。

useActionState と Zod を使った鉄壁のフォーム設計を解説しています。

▼ RSC とクライアントの切り分けを整理したいときに

Server Action をどこから呼ぶか(RSC / クライアントコンポーネント)は、RSC とクライアントの境界と一緒に整理すると迷いが減ります。

▼ 環境変数も「境界」で守りたいときに

Server Action 内で使う環境変数も、Zod で検証してから使う「境界」を決めておくと、起動時に不整合に気づけます。

ひとりごと

「境界」を決めてから書く、という順番は、コードを書く前に「うちのルール」を言語化する作業です。

最初は面倒に感じるかもしれませんが、一度決めておくと、あとから読み返したときに「ここでバリデーション、ここで認可」が一目でわかって、自分自身の負担が減ります。

あなたの Server Actions にも、小さな境界を一つ、決めてみてほしいです。

2026© おおとろ

ここから先は

0字

メンバーシップ ¥ 500 /月

■メンバーシップ 15 年目フリーランス開発者のひとり戦略を支える作戦会議室です。\ 案件に追われる…

ベーシックプラン

¥500 / 月

最後まで読んでいただき、ありがとうございます。もしこの記事が「役に立った」「心に響いた」と感じたら、珈琲一杯分サポートいただけると嬉しいです。あなたの温かい応援を力に、また次の創作活動に励みます。