🐯

server action 内に書くべきものと粒度の話

に公開

挨拶

こんばんは
盛者必衰の理を覆した男 身長188cm=>T.Miuraです.

ここ最近個人開発に熱が出ています

Next.jsを初めてはや6ヶ月.
ようやくまともにできるか?と言うレベルまで来ました
バカの山に後少しで登頂しそうです

はじめに

前述した通り私は初学者です.
慣習的なものもまだわかっていませんし,個人でやるにも規模に限界があります.

ただ,ありがたいことに一部reviewをしていただきました.
これまでの学習していたものが現場では通用しないことを痛感したので忘れないためにもこの記事を書いています

そして,この記事ではserver action に何をどこまで書くべきか?と言う話をします

これまで書いていたaction

export const createMeetup = async (
  _: ActionState<MeetupErrors>,
  formData: FormData,
): Promise<ActionState<MeetupErrors>> => {
  const rawFormData = {
    name: formData.get("name") as string,
    scheduledAt: formData.get("scheduledAt") as string,
  };

  const validatedFields = createMeetupSchema.safeParse(rawFormData);
  if (!validatedFields.success) {
    return {
      success: false,
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

  try {
    const user = await getUser();
    if (!user)
      return {
        success: false,
        errors: {
          auth: "認証に失敗しました",
        },
      };

    await prisma.meetup.create({
      data: {
        userId: user.id,
        name: validatedFields.data.name,
        scheduledAt: validatedFields.data.scheduledAt,
      },
    });
    return { success: true, errors: {} };
  } catch (error) {
    console.error(error);
    return {
      success: false,
      errors: {
        server: "server error",
      },
    };
  }
};

なぜこの構成か?

まず,私がこのActionに至った起源ですが,Udemy説が濃厚です.
Next.jsのとっかかりとして使用したUdemyではActionをこの書き方で行っていました.

私自身も小規模ならばこれで問題ない.
UI <-> Action(小規模logicも含む) だけでいいだろうと思っていました.

backend思想に則ってやるかぁ...?
いやぁ...?

と後回しにしていました(言い訳)

この構成での問題点

そもそも私のActionに必須なものは以下です

  1. validation
  2. 認証認可チェック(middlewareとは別に)
  3. ORM
  4. try-catch

私が感じた問題点1. 読みにくい

Action内でtry-catch から始まり
validation, Auth, ORM(throwされたらcatchされる)

これくらいの長さなら問題なかったのかもしれません(?)
しかし,単純なCRUDだけのアプリでもここまで短いものだけではありません.
プラスでドメインロジックもあるでしょう.
長いものでは数百行になることが予想されます.

私が感じた問題点2. testのしにくさ

Actionに詰め込みすぎてtest 書きにくかったです.

ORMのtestが描きたいだけなのにvalidation,Authを突破しなければいけない面倒さがありました(この時点で気がつけよという話ですが)

refactor後のコード

const handleCreateMeetup = async (
  formData: FormData,
): Promise<ActionState<MeetupErrors>> => {
  const raw = collectMeetupFormData(formData);

  const validated = validateMeetupFormData(raw);
  if (!validated.ok) {
    return {
      success: false,
      errors: validated.error,
    };
  }

  const user = await findAuthenticatedUser();
  if (!user.ok) {
    return {
      success: false,
      errors: user.error,
    };
  }

  const created = await createMeetupRecord(user.value.id, validated.value);
  if (!created.ok) {
    return {
      success: false,
      errors: created.error,
    };
  }

  return {
    success: true,
    errors: {},
  };
};

この変更により,Action内はUIからの呼び出し + UIへのreturn に注力することができます.

余分なtry-catchも関数に切り出して,非常に読みやすくなりました.

また,ORM呼び出し いわゆるRepository層に分けることでtestも楽に行えるようになりました.

review1: Action内の粒度がバラバラ

//直接FormDataの取得
  const rawFormData = {
    name: formData.get("name") as string,
    scheduledAt: formData.get("scheduledAt") as string,
  };
//直接呼び出し
  const validatedFields = createMeetupSchema.safeParse(rawFormData);
  if (!validatedFields.success) {
    return {
      success: false,
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }
	  //適切に分けられている
    const user = await getUser();
    if (!user)
      return {
        success: false,
        errors: {
          auth: "認証に失敗しました",
        },
      };
      
      //直接呼び出し
      await prisma.meetup.create({
      data: {
        userId: user.id,
        name: validatedFields.data.name,
        scheduledAt: validatedFields.data.scheduledAt,
      },
    });
  1. コード自体が短いから変わらないと言って直接呼び出しているものが3つ
  2. 適切に切り分けられたAuthチェック -> getUser()

この粒度が違うものが混在しているのが読みにくさの原因とコメントいただきました

短くても適切に関数に分けてAction側で呼び出すほうが良い

そして,次のreview につながります

review2: Actionは逐次処理のみにするほうがいい

ようは下記の連続ということです

const res1 = await 切り出した関数1()
    if (!res1)
      return {
        ...
        },
      };
const res2 = await 切り出した関数2()
    if (!res2)
      return {
        ...
        },
      };

つまり,責務の分離を適切にせよ と言うことです.

私自身めんどくさがり(絶対に治さなければいけないですが)なので短いものはそのまま書いてしまっていました.

しかし,ORM呼び出し等はtestに直結するので確実に分けたほうが良さそうですね

まとめ

だいぶかけるようになって来ました

そろそろ,動くからヨシ!フェーズからは抜け出してしっかりと設計できるように学んでいかなければいけないです.

今回のような責務の分離はNext.jsだけではなく,全てにおいて役立つことなので常に意識していきます(完全に忘れてた)

ここからはAIへ聞いた回答です

Next.js server Actionの設計思想について聞いたものです
あまり深追いはしないようにしていますが,一応置いておきます

---

# **Next.js の思想(FACT / SPECULATION / UNKNOWN)**

---

# **FACT(公式ドキュメントから読み取れる事実)**

### **1. UI(RSC)と副作用(Mutation)は分離されている**

- Server Component では副作用を行ってはならない。
    
- Mutation は Server Action で扱う。  
    **出典:** [https://nextjs.org/blog/security-nextjs-server-components-actions](https://nextjs.org/blog/security-nextjs-server-components-actions?utm_source=chatgpt.com)
    

### **2. Server Action は “UI から呼ばれるサーバ側の Mutation Handler”**

- UI から呼び出される非同期関数。
    
- form submission やデータの更新を担う。  
    **出典:** [https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)
    

### **3. RSC により UI と Server の距離が縮まり、責務分離が強化されている**

- RSC はデータ取得と UI の結合を許容する。
    
- しかし Mutation を含めることは許されない。
    
- 結果的に “UI(読み取り)” と “Action(変更)” が強制的に分かれる構造になる。
    

### **4. Server Action はテストが困難で、fat にするとさらに難易度が上がる**

- RSC バンドル依存があり、Action 単体のテストが難しいと公式が明記。
    
- fat Action は Next.js のテスト戦略と相性が悪い。  
    **出典:** [https://nextjs.org/blog/security-nextjs-server-components-actions](https://nextjs.org/blog/security-nextjs-server-components-actions?utm_source=chatgpt.com)
    

---

# **SPECULATION(設計思想として読み取れる推測)**

### **1. Server Action は Application 層(UseCase 的役割)に近い**

- UI から呼ばれ副作用のみを担うため、  
     “UI と Domain の橋渡し” という Application 層的な位置づけに近い。
    
- Domain や Repository を内包させるより、外へ分離した方が適合する構造。
    

### **2. Next.js は Controller 的な fat 層を避けている**

- 明確な Controller を持たず、  
     UI(RSC)・Action(Mutation)・Data 層を分ける設計になっている。
    
- これは伝統的 MVC の Controller を再現する意図が弱いと推測できる。
    

### **3. Action は長期的にも “薄いレイヤー” として扱われる可能性が高い**

- RSC による読み取りと Suspense による UI モデルが強固。
    
- その結果、Action は「Mutation の指揮役」に限定される傾向が強い。
    

### **4. App Router は結果として Clean Architecture に“近い構造”になっている**

- UI(RSC)
    
- Application(Server Action)
    
- Domain / Repository(ユーザー定義)  
    という役割分離が自然に発生する。
    

ただしこれは **機能構造の結果として“似ている”** という推測であり、  
Next.js が Clean Architecture を採用したとは一切言っていない。

---

# **UNKNOWN(Next.js 公式が明言していないため不明)**

### **1. Next.js が Action を UseCase 層として設計したかどうか**

- 公式は UseCase / Application 層という語を使用していない。
    

### **2. Action を薄く保つことを公式が強制するかどうか**

- 粒度に関するガイドラインは存在しない。
    

### **3. Next.js が Clean Architecture を参考にしたかどうか**

- Clean Architecture の名称は公式に一切登場しない。
    

### **4. Domain / Repository 層の正式な構成を Next.js が定義するかどうか**

- Router と UI 以外のアプリケーション構造はユーザーの裁量に任されている。
    

---

# **まとめ**

**FACT**

- UI と副作用の分離
    
- Action は Mutation Handler
    
- RSC による責務分離の強制
    
- fat Action はテストと相性が悪い
    

**SPECULATION**

- Action は UseCase 的ポジション
    
- Next.js は fat Controller を避ける傾向
    
- Action は薄くあるべき構造
    
- App Router は Clean Architecture に結果として近い
    

**UNKNOWN**

- 公式が UseCase として意図したか
    
- Clean Architecture を採用したか
    
- Action 粒度の正式なガイドライン

Discussion