Open22

ローコードプラットフォーム開発ログ

ymtdirymtdir

開発するもの

Next.js製の業務アプリケーションプラットフォーム。
ノーコード/ローコードで業務アプリを構築できるWebサービス。

開発理由

ユーザーが自由にアプリケーションを構築できる基盤を開発してみたい。
既存のローコードアプリケーションよりも柔軟なカスタマイズ機能を持たせたい。

アプリケーション名は後から考える。
https://github.com/ymtdir/lowcode-platform

ymtdirymtdir

Next.jsの導入

公式ドキュメントを見ながらセットアップ。
https://nextjs.org/docs/app/getting-started/installation

事前にリポジトリをクローンしてディレクトリ作ってあったので、カレントディレクトリを指定してインストール。

npx create-next-app@latest .

依存関係インストール完了したので起動。

npm run dev

http://localhost:3000/ にアクセスできたのでNext.jsのセットアップ完了!
次はshadcn/uiのセットアップをしていく。

ymtdirymtdir

shadcn/ui の導入

こちらも公式ドキュメントを見ながらセットアップ。
https://ui.shadcn.com/docs/installation/next

npx shadcn@latest init

またもやコマンド一発で完了。
Viteのときはもっと工程多かった気がするけど、Next.jsだと楽すぎる。

base colorはひとまず Neutral を選択。

どうせ使うだろうからbuttonコンポーネントだけインストールしておいた。

npx shadcn@latest add button
ymtdirymtdir

堅牢性と品質のためのTSConfig設定

tsconfig.json の compilerOptions に以下の厳格なチェックを設定する。

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "forceConsistentCasingInFileNames": true
  }
}

設定の意図(これって何のため?)

設定 目的と効果 こんな感じらしい
"strict": true 型チェックをより厳密にする TypeScriptの型安全性を最大限に活かすための基本設定。
"noImplicitAny": true 暗黙的な any 型を禁止 型を省略した変数に自動でanyがつくのを防ぎ、明示的な型指定を促す。
"forceConsistentCasingInFileNames": true ファイル名の大文字・小文字の違いを検出 OS間の大小文字差によるビルドエラーを防ぐ安全策。
ymtdirymtdir

Prettier の導入と ESLint 連携

Next.jsプロジェクトのコードスタイルを統一するため、ESLint (標準搭載) に加えて Prettier を導入し、最新の ESLint Flat Config 形式で連携させる。

1. Prettier と 競合解消パッケージのインストール

まず、Prettierをインストールする。

npm install -D prettier

ESLint と Prettier の整形ルールが競合しないようにする設定をインストールする。

npm i -D eslint-config-prettier

💡 解説: eslint-config-prettier は、ESLint の整形に関するルール(例: インデント、セミコロンの有無、クォートの種類など)をすべて無効化するための設定です。
これにより、ESLint と Prettier の間でスタイルが衝突するのを防ぎ、Prettier に整形を一任できます。

2. Prettier の設定ファイル作成

プロジェクトルートに .prettierrc.json を作成し、モダンな開発で推奨される設定を適用。

{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": true,
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

3. フォーマットコマンドの設定

既存の package.json の "scripts" セクションに、以下のハイライト部分を追記します。

💡 解説: format コマンドは、スタイルを統一するためのもので、構文エラーや命名規則の不備といったロジックや品質のエラーを検出するのは、引き続き lint コマンド (ESLint) の役割です。

{
  // ...
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    // ↓コマンドを追加
    "format": "prettier --write ."
  },
  // ...
}

4. Prettier 連携を組み込んだ ESLint 設定

eslint.config.mjs

import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import prettier from "eslint-config-prettier/flat";

const eslintConfig = defineConfig([
  ...nextVitals,
  ...nextTs,
  prettier,
  // Override default ignores of eslint-config-next.
  globalIgnores([
    // Default ignores of eslint-config-next:
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
  ]),
]);

export default eslintConfig;

5. フォーマットの実行

設定が完了したら、package.json に追加した format コマンドを実行して、プロジェクト全体を一度に整形。

npm run format
ymtdirymtdir

Claude CodeのMCP設定

自分の過去記事を見ながら Claude Code に GitHub MCPサーバーを設定。
https://qiita.com/ymtdir/items/4d17b5ebac0b025eb825

.mcp.json

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}",
        "GITHUB_OWNER": "ymtdir",
        "GITHUB_REPO": "documorph"
      }
    }
  }
}

これでClaude CodeからDocumorphリポジトリのIssueやPRを直接操作できるようになった。
とは言いつつ今回はなるべく手動で書いていく予定。

ymtdirymtdir

IssueテンプレートとPRテンプレート作成

自分の過去記事を見ながらGitHubのテンプレートを設定。
https://qiita.com/ymtdir/items/b1c349fdb77c78389e23

作成したファイル

.github/
├── ISSUE_TEMPLATE/
│   ├── bug_template.md
│   └── enhancement_template.md
└── pull_request_template.md
ymtdirymtdir

CLAUDE.mdの作成

Claude Codeが最初に読んでくれるファイルで、プロジェクトの全体像・技術スタック・ディレクトリ構成とかをまとめておくと後が楽。

Next.jsのベストプラクティスについては以下の記事を参考にしつつ、
Claudeと壁打ちしながら多少時間をかけて作成。

https://zenn.dev/akfm/books/nextjs-basic-principle

ymtdirymtdir

フォーマット時のルール設定

コードの統一性を保つため、PrettierとESLintを設定。

Prettier

整形ルール

.prettierrc

{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 80,
  "tabWidth": 2,
  "arrowParens": "always"
}

各設定:

  • semi: 文末セミコロン
  • trailingComma: 末尾カンマ(ES5準拠)
  • singleQuote: シングルクォート使用
  • printWidth: 1行80文字まで
  • tabWidth: インデント2スペース
  • arrowParens: アロー関数の引数に括弧

整形しないファイルの設定

.prettierignore

# Dependencies
node_modules/

# Build outputs
.next/
out/
dist/
build/

# Coverage reports
coverage/

# Static assets
public/

# TypeScript build cache
*.tsbuildinfo

# Log files
*.log

# Lock files
*.lock
package-lock.json
pnpm-lock.yaml
yarn.lock

# Environment files
.env*
.vercel/

# IDE / OS files
.vscode/
.idea/
.DS_Store
Thumbs.db

# Git
.git/

ESLint(コード品質チェック)

.eslintrc.json

{
  "extends": ["next/core-web-vitals", "plugin:prettier/recommended"]
}

Next.js推奨ルール + Prettier連携。シンプルにデフォルト設定のまま使用。

ymtdirymtdir

Supabaseをセットアップ

公式ドキュメントを見ながらセットアップ。
https://supabase.com/docs/guides/getting-started/quickstarts/nextjs

新たにプロジェクトを作成するのではなく、既存プロジェクトにSupabaseの設定を追加したため多少手順が違かった。

1. プロジェクト内で依存関係をインストール

npm install @supabase/supabase-js @supabase/ssr

2. Supabaseプロジェクトの作成

  1. 下記URLからSupabaseに会員登録 & ログイン
  2. プロジェクトを作成し、Project URLとPublishable keyを取得

https://supabase.com/

3. 環境変数の設定

.env

NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-anon-key

4. サンプルデータの作成

SQL Editorでサンプルクエリを実行してinstrumentsテーブルとサンプルレコードを作成。

5. 接続確認

  • lib/supabase/server.ts ,app/instruments/page.tsx を作成
  • npm run devでアプリケーションを起動
  • http://localhost:3000/instruments にアクセスし、データ表示を確認
ymtdirymtdir

Prismaの導入

PrismaはNode.js/TypeScript向けのORMツール。
データベース操作を型安全に行えるらしいので、Supabaseと併用して導入してみる。

1. 依存関係のインストール

npm install prisma @prisma/client

2. Prismaの初期化

npx prisma init

3. 環境変数の設定

.envにDATABASE_URLを追記

DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"

画面上部のConnectボタンから接続文字列を取得できた

ymtdirymtdir

トラブルメモ

npx prisma init実行時に以下の問題が発生:

  • .envファイルにDATABASE_URLが自動で追記されなかった
  • prisma.config.tsという不要なファイルが生成された
  • schema.prismaprovider"prisma-client"になっていた(正しくは"prisma-client-js"

原因は不明だが、手動で修正して対応。

ymtdirymtdir

Prismaスキーマの定義

prisma/schema.prismaにUserモデルを追加。
Prismaではユーザーの追加情報(名前、ロール等)のみを管理して、認証関係のことはSupabase Auth が上手いことやってくれるらしい。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String?
  role      UserRole @default(MEMBER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum UserRole {
  ADMIN
  MEMBER
}

データベースに反映

サンプルで作成したテーブルをリセットしてから、正式なマイグレーションを実行。

# 既存のテーブルをすべて削除
npx prisma migrate reset

# 初回マイグレーションを実行
npx prisma migrate dev --name init

サンプル用に作成していたapp/instruments/page.tsx等のファイルも削除しておく。

ymtdirymtdir

Prisma Clientを作成

Prismaのクライアントを初期化するファイルを作成。
Next.jsなどの開発環境ではホットリロード時にコードが何度も再実行されるため、
接続が増えすぎないようシングルトンパターンで実装するらしい。

lib/prisma.ts

import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

コードの補足

globalThis(グローバルオブジェクト)には本来prismaプロパティは存在しないが、
as unknown asで無理やりキャストすることで、TypeScriptの型チェックを通している。
実行時に動的にプロパティを追加するためのテクニック。

// ❌ これはエラー(直接キャストできない)
globalThis as { prisma: PrismaClient | undefined }

// ✅ 一旦unknownを経由すれば何にでもキャストできる
globalThis as unknown as { prisma: PrismaClient | undefined }
ymtdirymtdir

Supabase Auth(認証機能)の統合

公式ドキュメントに沿って実装。
https://supabase.com/docs/guides/auth/server-side/nextjs

変更点:

ログイン後のページのディレクトリ名をprivateからdashboardに変更。
認証範囲を /dashboard 配下に限定。

  • ディレクトリ名の変更
    • app/private/app/dashboard
  • ミドルウェアの修正
    • lib/supabase/middleware.ts
      // 認証が必要なページ(/dashboard配下)
      if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
      const url = request.nextUrl.clone();
      url.pathname = '/login';
      return NextResponse.redirect(url);
      }
      
      // ログイン済みユーザーが認証ページにアクセスした場合
      if (request.nextUrl.pathname.startsWith('/login') && user) {
      const url = request.nextUrl.clone();
      url.pathname = '/dashboard';
      return NextResponse.redirect(url);
      }
      
  • ログイン成功時のリダイレクト先を変更
    • app/login/actions.ts(2箇所)
      - redirect('/');
      + redirect('/dashboard');
      
ymtdirymtdir

トラブルメモ

xxx@example.comのようなテスト用メールアドレスはSupabase側でバリデーションエラーになった。
回避策はあるらしいが、一旦実際のメールアドレスを使用することにした。

Gmailの+エイリアス機能を使うことで、同じ受信箱に届きつつ別アドレスとして登録できるらしいので、ひとまずこれで進めていく。

myaddress+1@gmail.com
myaddress+2@gmail.com
ymtdirymtdir

トラブルメモ

メール確認が必須になっていた

Supabaseでメール確認が有効になっており、確認メールのリンクをクリックしないとログインできなかった。

解決方法:

  1. Supabaseダッシュボードにアクセス
  2. Authentication → `Sign in / Provuders
  3. Confirm email を OFF にする

追記:
気が付いたらxxx@example.comのようなテスト用メールアドレスが使用可能になっていた。
恐らく上記設定をしたおかげ。

Supabase AuthとPrismaの整合性問題

Supabase AuthからユーザーをWeb上で削除しても、Prismaには残ったままで整合性が取れず面倒だった。

対処法:

npx prisma studio

Prisma StudioのGUIからもユーザーを削除する必要がある。

ちゃんと削除処理を実装すれば解決するはず

ymtdirymtdir

ログイン/サインアップフォームのUI実装

認証機能が動くようになったので、UIを整える。
shadcn/ui の Cardコンポーネントを使って、ログイン・サインアップフォームを作成した。
https://ui.shadcn.com/docs/components/card

まずはマニュアル通りにコンポーネントを追加。
その後、テキストを日本語に変更したり、ボタンやリンクの遷移先を自分の環境に合わせて微調整した。

ymtdirymtdir

Google認証を実装

shadcn/uiのマニュアル通りにログインフォームを実装したら、「Googleでログイン」というボタンがあった。
ということで、せっかくなのでGoogle認証も実装してみることにした。

1. Google Cloud Consoleにアクセス

https://console.cloud.google.com/

Supabaseと同じプロジェクト名で作成しておいた。

2. OAuth同意画面の設定

  1. 左メニュー「APIとサービス」→「OAuth同意画面」
  2. 作成したプロジェクトを選択
  3. 「開始」ボタンをクリックし、アプリ情報を入力
  4. 対象に「外部」を選択
  5. 連絡先情報を入力して完了

3. OAuthクライアントの作成

  1. 「OAuthクライアントを作成」をクリック
  2. アプリケーションの種類を「ウェブアプリケーション」に設定
  3. 名前を入力し、承認済みリダイレクトURIにSupabaseのコールバックURLを追加
  4. IDを作成し、表示されたクライアントIDとクライアントシークレットを控えておく

4. SupabaseでGoogle認証を有効化

  1. Supabase Dashboardで「Authentication」→「Providers」→「Google」を開く
  2. 先ほどコピーしておいたクライアントIDとクライアントシークレットを入力
  3. 設定を保存して有効化

5. アプリケーションに認証処理を実装

最後にアプリケーションに認証周りの処理を実装し、Google認証が使えるようになった。
Supabaseは他にもGitHub、Discord、Twitterなど様々なプロバイダーに対応していて便利すぎる...

ymtdirymtdir

サイドバーの作成

ログイン後のダッシュボードを作るにあたり、共通のサイドバーを作ることにした。
https://ui.shadcn.com/docs/components/sidebar

とりあえずモックとして見た目だけ作成し、ダッシュボード全体に適用。

layout.tsxの階層構造

Next.jsではlayout.tsxをディレクトリごとに配置できることがわかった。
これにより、ログインページにはサイドバーを表示せず、ダッシュボード配下(/dashboard)だけに適用できる。

app/
├── layout.tsx          # 全体共通レイアウト
├── login/
│   └── page.tsx        # サイドバーなし
└── dashboard/
    ├── layout.tsx      # サイドバー付きレイアウト
    └── page.tsx        # サイドバーあり

ディレクトリ構成も迷いにくく、中々便利な仕組み。

ymtdirymtdir

ダークモードの実装

https://ui.shadcn.com/docs/dark-mode/next

shadcn/uiの公式ドキュメント通りにダークモードを実装。
next-themesを使ってライト/ダーク/システムの3つのテーマ切り替えに対応した。

マニュアル通りに進めるだけで特に詰まることなく実装完了。
テーマの切り替えトグルは、ドキュメントのサンプルをほぼほぼそのまま使っているが、後ほど変更予定。

ymtdirymtdir

ユーザーメニューの実装

テーマの切り替えトグルはユーザー設定画面に配置したいと考え、ユーザーメニューとユーザー設定ダイアログを実装。

コンポーネントをどこまで分けるかなど迷ったがこのような構成にした。

features/layout/components/
├── app-sidebar.tsx
└── user-menu/
    ├── index.tsx # UserMenuコンポーネント(統合)
    ├── account-settings-dialog.tsx # ダイアログコンテンツ
    ├── account-settings-item.tsx # Dialog + Trigger
    └── logout-item.tsx # ログアウトメニューアイテム

index.tsxはディレクトリ名を使えるとのことで、コンポーネントの親子関係が管理しやすいと感じた。
別コンポーネントからの呼び出しもシンプル。

// app-sidebar.tsx
import { UserMenu } from './user-menu';  // user-menu/index.tsx が読み込まれる

<DropdownMenuContent side="top">
  <UserMenu />  // シンプル!
</DropdownMenuContent>