myfit プロジェクトの Next.js コーディング規約。Next.js 16 + React 19 + App Router + Elysia + Tailwind CSS v4 + shadcn/ui + Firestore の構成におけるファイル配置、コンポーネント設計、スタイリング、API設計、型定義、データモデルのルールを定義。 このプロジェクトで .tsx/.ts ファイルを作成・編集するとき、コンポーネントやページを追加するとき、APIエンドポイントを追加するとき、型定義やスキーマを扱うときに使用する。
日本語でコメント・説明を記述する。
パスエイリアス: @/* でプロジェクトルート参照。
'use client' はインタラクティブな機能(useState, useEffect, イベントハンドラ、ブラウザAPI)が必要な場合のみ付与components/
├── ui/ # shadcn/ui(自動生成)
├── common/ # 共通(Header, Footer 等)
└── pages/ # ページ固有(ページ名サブディレクトリ: pages/dashboard/stats-card.tsx)
stats-card.tsx)StatsCard)onXXX (onClick, onSubmit, onChange)useXXX (useAuth, useForm)interface を使うtype ではなく interface を優先する。
// Good
interface User {
id: string
name: string
}
// Bad
type User = {
id: string
name: string
}
ユニオン型やユーティリティ型など interface で表現できないものは type を使ってよい。
バレルファイル(index.ts)による再エクスポートは使わない。各ファイルから直接インポートする。
// Good
import { User } from "@/model/user"
// Bad
import { User } from "@/model" // index.ts 経由
model/ ディレクトリで Firestore のスキーマ(コレクション構造・型定義)を管理する。
model/
├── user.ts # users コレクションの型定義
├── workout.ts # workouts コレクションの型定義
└── ...
各ファイルでコレクションに対応する interface を定義する。
shadcn/ui (@/components/ui/*) を優先的に使い、素の HTML 要素をなるべく使わない。
// Good
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
<Button onClick={onSubmit}>送信</Button>
// Bad - 素の HTML を使っている
<button onClick={onSubmit}>送信</button>
<input type="text" />
shadcn/ui に該当コンポーネントがない場合のみ素の HTML を使う。未追加のコンポーネントは npx shadcn@latest add <name> で追加する。
動的クラス結合には必ず cn() を使う。
// Good
<div className={cn("flex items-center", isActive && "bg-primary")} />
// Bad
<div className={`flex items-center ${isActive ? "bg-primary" : ""}`} />
静的クラスのみなら className 直書き OK。色は CSS 変数(bg-primary, text-muted-foreground 等)を使い、ハードコード禁止。
server/
├── index.ts # エントリポイント(ルート集約)
└── routes/
├── health.ts # /api/health
├── users.ts # /api/users
└── workouts.ts # /api/workouts
// server/routes/users.ts
import { Elysia } from "elysia"
export const usersRoute = new Elysia({ prefix: "/users" })
.get("/", () => { /* 一覧 */ })
.post("/", () => { /* 作成 */ })
.get("/:id", ({ params }) => { /* 詳細 */ })
// server/index.ts
import { Elysia } from "elysia"
import { usersRoute } from "./routes/users"
const app = new Elysia({ prefix: "/api" })
.use(usersRoute)
export type App = typeof app
export default app
app/
├── layout.tsx
├── page.tsx
├── (auth)/ # Route Group
│ ├── login/page.tsx
│ └── register/page.tsx
├── dashboard/
│ ├── layout.tsx
│ └── page.tsx
└── api/[[...route]]/route.ts # Elysia catch-all(変更しない)
(name) で URL に影響なくレイアウト共有loading.tsx, error.tsx は必要に応じてページ単位で配置