nino
  • Docs
  • Registry
  • Membership

Command Palette

Search for a command to run...

スポンサー
メニュー

コードの整理

 

7m 34s

shadcn/ui を踏襲し、レイヤード構成でコードを整理しています。

ディレクトリ構成

actions# サーバーアクション
TypeScriptroute.ts# ルートハンドラー
TypeScriptpage.tsx# 画面
ui# shadcn/ui のコンポーネント
data# データ取得関数
schemas# Drizzleスキーマ
e2e# E2Eテスト
hooks# 共通hooks
TypeScriptauth.ts# 認証関連のロジック
swr# SWR のカスタムフック
tests# 単体テスト
types# 共通型定義
zod# Zod スキーマ

コロケーションのルール

特定の画面に依存するコードは、その画面のディレクトリに配置します。ただし、少しでも再利用の余地がある場合 /components に配置します。

TypeScriptprofile-form.tsx# プロフィール画面でのみ使用するフォーム
TypeScriptpage.tsx

データ取得(サーバー)

データ取得ロジックは、/data に配置します。あらゆるデータ取得は必ず /data の関数を使用します。これによりセキュリティリスクを低減します。

TypeScripttask.ts# タスクデータ取得
TypeScriptuser.ts# ユーザーデータ取得

データ更新(作成、更新、削除)

変更系の処理は Server Actions として /actions に配置します。あらゆるデータ更新は必ず /actions の関数を使用します。これによりセキュリティリスクを低減します。

TypeScripttask.ts# タスク作成、更新、削除
TypeScriptuser.ts# ユーザー作成、更新、削除

データ取得(クライアント)

クライアントからデータ取得を行う場合は、/swr に SWR のカスタムフックを配置します。クライアントからデータ取得する際は必ず /swr の関数を使用します。また、SWR用のルートハンドラーも /app/api に配置します。ルートハンドラーは内部的に /data の関数を使用します。

TypeScriptroute.ts# タスクデータ取得
TypeScripttask.ts# タスクデータ取得

Zod スキーマ

Zod スキーマは、/zod に配置します。Zod スキーマは基本的に drizzle-zod を使用して Drizzle スキーマに基づいて定義します。

TypeScripttask.ts# タスクZodスキーマ
Loading...

型

型は、/types に配置します。型は基本的に Drizzle スキーマに基づいて定義します。

TypeScripttask.ts# タスク型
Loading...

コンポーネントファイルの粒度

単体で export する必要がない構成コンポーネントは、そのファイル内で直接定義します。

Loading...

use client で区切る場合や、一つのファイルに書くと極端に長くなる場合(概ね400行以上)は、ファイルを分割します。

Loading...

Features 構成との比較

Features 構成とは、機能ごとにディレクトリを分ける構成です。

TypeScriptroute.ts# タスクAPIルートハンドラー
TypeScripttask-form.tsx# タスク画面でのみ使用するフォーム
TypeScriptactions.ts# タスク作成、更新、削除
TypeScriptdata.ts# タスクデータ取得
TypeScriptpage.tsx# タスク画面
TypeScriptswr.ts# タスクSWRフック
TypeScripttypes.ts# タスク型
TypeScriptutils.ts# タスク機能のユーティリティ関数
TypeScriptzod.ts# タスクZodスキーマ

レイヤード構成を採用することで、以下のようなメリットがあります。

コードの場所が予測可能

レイヤード構成では、機能ごとにディレクトリを分けるのではなく、責務ごとにディレクトリを分けます。これにより、コードの場所が予測可能になり、新しいメンバーでも素早くコードベースを理解できます。

セキュリティリスクの低減

データ取得は /data、データ更新は /actions に一元化することで、すべてのデータアクセスを制御できます。これにより、意図しないデータアクセスやセキュリティホールを防ぐことができます。

再利用性の向上

共通のコンポーネントやロジックを /components や /lib に配置することで、コードの再利用性が向上します。また、特定の画面に依存するコードは画面のディレクトリに配置することで、適切なスコープでコードを管理できます。

保守性の向上

責務ごとにディレクトリを分けることで、変更の影響範囲が明確になります。例えば、データベーススキーマを変更する場合は /db/schemas のみを確認すればよく、型定義を変更する場合は /types のみを確認すればよいため、保守性が向上します。

一貫性の確保

チーム全体で同じディレクトリ構成を採用することで、コードレビューやペアプログラミングの際に、コードの場所についての認識のずれが発生しにくくなります。

Features 構成の課題

ドメイン知識の必要性

Features 構成では、コードを配置する際に「どの機能に属するか」を判断する必要があります。そのため、プロジェクトのドメイン知識が十分でないメンバーがコードを配置する際に、適切な場所を見つけることが困難になる場合があります。

機能の境界が曖昧な場合の破綻

機能の境界が曖昧な場合、同じようなコードが複数の機能ディレクトリに散在してしまう可能性があります。例えば、「ユーザー管理」と「プロフィール管理」の境界が曖昧な場合、どちらのディレクトリにコードを配置すべきか判断に迷い、コードの重複や不整合が発生しやすくなります。

メンバー間の解像度の差による破綻

チームメンバー間で機能の解像度(機能の粒度や範囲の理解)に差がある場合、同じ機能でも異なるディレクトリにコードが配置されてしまう可能性があります。これにより、コードの場所が予測不可能になり、コードベースの理解が困難になります。

レイヤード構成の優位性

レイヤード構成では、責務ごとにディレクトリを分けるため、ドメイン知識がなくても「データ取得なら /data」「コンポーネントなら /components」というように、明確な判断基準に基づいてコードを配置できます。また、機能の境界が曖昧でも、責務は明確なため、コードの配置に迷うことが少なくなります。

Discord で質問する
何か気になったこと、分からないことがあれば気軽に質問してください!
DiscordDiscordで質問する
nino

Developer

XXGitHubGitHubYouTubeYouTubeZennZennDiscordDiscord

    Links

  • Documentation
  • Registry
  • Architecture

    Tools

  • Feed
  • Status

    Policies

  • Terms of Service
  • Privacy Policy
  • Legal
Documentation
Getting Started
  • Changelog
Guides
  • Webアプリの環境構築
  • ニュース収集&AI要約
  • Turso のテーブル移行
  • 日時の管理
  • 中規模のリストをブラウザにキャッシュし、クライアントでフィルタする
  • Search Params Dialog
  • コードの整理
  • AGENTS.md
  • Better Auth
  • プロダクト開発ポリシー
  • 推奨ツール
  • Proxy.ts
  • 多言語対応
  • SWR
  • Cache Component
  • Next.js の課題、不具合
  • アプリ開発フロー
  • プロンプトガイド
  • CSS Tips
  • Font Family
  • セルフブランディング
  • セルフオンボーディング
  • オフィスツール
  • Email
  • Breadcrumb(パンくず)
  • 並べ替え
import { z } from "zod";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { tasks } from "../db/schemas/task";

// データ挿入用のZodスキーマ
export const insertTaskSchema = createInsertSchema(tasks, {
  title: z.string().trim()
    .min(1, "タイトルを入力してください")
    .max(255, "タイトルは255文字以内にしてください"),
});
import { InferInsertModel, InferSelectModel } from "drizzle-orm";
import { tasks } from "@/db/schemas/task";

export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;
export function UserList() {
  return <ul>{users.map((user) => <UserItem key={user.id} user={user} />)}</ul>;
}

function UserItem() {
  return <li>...</li>;
}
import { ProfileForm } from "./profile-form";

export default function ProfilePage() {
  return <div>
    <ProfileForm />
  </div>;
}