AdminJSで管理画面を爆速で作る!

に公開

本記事のサマリ

Next.js + Prismaのプロジェクトに、AdminJSを使って管理画面を実装した記録です。App Routerには公式対応していないため、別ポートでExpressサーバーを立ち上げる方式を採用しました。tiptapの依存関係トラブルも含めて、実際に遭遇した課題と解決策をまとめています。

今回の対応内容↓

https://github.com/toto-inu/lab-202511-vercel-nextjs/pull/1/files

なぜAdminJSなのか

管理画面って、作るのが面倒なんですよね。ユーザーが直接触る部分じゃないからデザインにそこまでこだわる必要もないし、CRUD操作ができればとりあえず十分。でも一から作ると意外と時間がかかる。テーブルの一覧表示、フィルター機能、ページネーション、フォームバリデーション...考えることが多すぎます。

そこで出会ったのがAdminJSでした。このライブラリの魅力は、なんといってもPrismaとの統合の良さです。既存のPrismaスキーマから自動的にCRUD画面を生成してくれるので、設定さえ書けばすぐに管理画面ができあがります。

今回のプロジェクトの技術スタックは以下の通りです:

  • フレームワーク: Next.js 16 (App Router)
  • パッケージマネージャー: Bun
  • データベース: PostgreSQL (Docker)
  • ORM: Prisma
  • 管理画面: AdminJS 7.5.0

実装の全体像

AdminJSはNext.js App Routerに公式対応していないため、少し工夫が必要でした。結果的に採用したのは、Next.jsアプリケーション(3000ポート)とは別に、AdminJS用のExpressサーバー(3001ポート)を立ち上げる方式です。

ディレクトリ構成はこんな感じになります:

lab-202511-vercel-nextjs/
├── app/                    # Next.jsアプリケーション (port 3000)
│   ├── admin/             # AdminJS Expressサーバー (port 3001)
│   │   ├── server.ts      # サーバーエントリーポイント
│   │   ├── package.json   # Admin用設定
│   │   └── tsconfig.json  # TypeScript設定
│   ├── prisma/
│   │   └── schema.prisma  # データベーススキーマ
│   └── package.json       # メインパッケージ設定
└── docker-compose.yml     # PostgreSQL設定

この構成なら、メインのNext.jsアプリケーションに影響を与えることなく、管理画面だけを独立して動かせます。

セットアップ手順

パッケージのインストール

まずは必要なパッケージをインストールします:

cd app
bun add adminjs@7.5.0 @adminjs/prisma@5.0.1 @adminjs/express@6.1.0
bun add express express-formidable express-session
bun add @types/express @types/express-session
bun add -d tsx

ここで重要なのは、AdminJSのバージョンを明示的に指定することです。7.5.0を使うことで、後述するtiptapの依存関係問題を避けやすくなります。

tiptap依存関係の闇と解決

インストール直後に起動しようとすると、こんなエラーに遭遇しました:

SyntaxError: Export named 'canInsertNode' not found in module '@tiptap/core'

調べてみると、AdminJSの内部で使用している @adminjs/design-systemが、古いバージョンの @tiptap/core@2.1.13を参照しており、そのバージョンには canInsertNodeというエクスポートが存在しないのが原因でした。

解決策として、以下の手順で依存関係を統一しました:

bun add @adminjs/design-system@4.1.1
bun add @tiptap/core@3.11.1 @tiptap/pm@3.11.1
bun add @tiptap/extension-horizontal-rule@3.11.1 @tiptap/starter-kit@3.11.1

さらに、package.jsonresolutionsフィールドを追加して、バージョンを強制的に統一します:

{
  "resolutions": {
    "@tiptap/core": "3.11.1",
    "@tiptap/pm": "3.11.1"
  }
}

最後に node_modulesをクリーンインストールします:

rm -rf node_modules && bun install

この手順により、ネストされた依存関係も含めて一貫したバージョンを使用できるようになります。

AdminJSサーバーの構築

次に、管理画面のサーバーを構築します。admin/server.tsに以下のコードを書きました。少し長いですが、順を追って解説していきます。

1. 必要なモジュールのインポート

import AdminJS from 'adminjs'
import AdminJSExpress from '@adminjs/express'
import express from 'express'
import { Database, Resource, getModelByName } from '@adminjs/prisma'
import { PrismaClient } from '@prisma/client'

ここで重要なのは @adminjs/prismaからインポートしている3つです:

  • Database: Prismaのデータベースアダプターのクラス
  • Resource: Prismaのモデルをリソースとして扱うためのクラス
  • getModelByName: Prismaスキーマからモデル定義を取得する関数

この getModelByNameが、後ほど「TodoモデルをAdminJSで扱えるようにする」ために使います。

2. PrismaClientの初期化とアダプター登録

const PORT = 3001

// Initialize Prisma Client
const prisma = new PrismaClient()

// Register the Prisma adapter
AdminJS.registerAdapter({ Database, Resource })

AdminJS.registerAdapter()の呼び出しが必須です。これを忘れると、AdminJSがPrismaのモデルを認識できません。このアダプター登録により、AdminJSは「Prismaのモデルをどう扱うか」を理解できるようになります。

3. AdminJSインスタンスの作成

ここからが本丸です。AdminJSの設定は resources配列に記述します:

const admin = new AdminJS({
  resources: [
    {
      resource: { model: getModelByName('Todo'), client: prisma },
      options: {
        // リソースごとの設定
      },
    },
  ],
  rootPath: '/admin',
  branding: {
    // ブランディング設定
  },
})

resourceフィールドの説明:

resource: { model: getModelByName('Todo'), client: prisma }

getModelByName('Todo')は、Prismaスキーマで定義した model Todoを取得します。この時点で、Todoテーブルのカラム情報やリレーション情報がAdminJSに渡されます。

optionsフィールドでは、このリソースをどう表示・操作するかを細かく設定できます。

4. navigationの設定

navigation: {
  name: 'Database',
  icon: 'Database',
}

これは左サイドバーでの表示グループを指定します。name: 'Database'とすることで、複数のリソース(TodoとAssignee)を「Database」というグループにまとめています。

5. propertiesの詳細設定

各フィールドの表示方法を制御します:

properties: {
  id: {
    isVisible: { list: true, filter: true, show: true, edit: false },
  },
  title: {
    isRequired: true,
    isTitle: true,
  },
  description: {
    type: 'textarea',
  },
  completed: {
    type: 'boolean',
  },
}

idフィールドの設定

  • list: true: 一覧画面に表示する
  • filter: true: フィルター条件として使える
  • show: true: 詳細画面に表示する
  • edit: false: 編集画面では非表示(IDは自動生成されるので編集不要)

titleフィールドの設定

  • isRequired: true: 必須入力項目にする
  • isTitle: true: このフィールドをレコードの代表名として使う(一覧で太字表示される)

descriptionフィールドの設定

  • type: 'textarea': 複数行のテキストエリアで入力できるようにする(デフォルトは1行テキスト)

completedフィールドの設定

  • type: 'boolean': チェックボックスで表示する

これらの設定を省略すると、AdminJSがPrismaのスキーマから推測して自動設定してくれますが、明示的に書くことで意図した動作を保証できます。

6. ブランディング設定

branding: {
  companyName: 'Todo App Admin Panel',
  withMadeWithLove: false,
  logo: false,
  theme: {
    colors: {
      primary100: '#4f46e5',
      primary80: '#6366f1',
      primary60: '#818cf8',
    },
  },
}

withMadeWithLove: falseで「Made with ♥ by AdminJS」の表示を消し、theme.colorsでプライマリカラーを設定します。Indigo系の色を選んだことで、落ち着いた印象の管理画面になりました。

7. Expressとの統合

const adminRouter = AdminJSExpress.buildRouter(admin)
app.use(admin.options.rootPath, adminRouter)

app.listen(PORT, () => {
  console.log(`✓ AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)
})

buildRouter()で、AdminJSの設定をExpressのルーターに変換します。app.use()/adminパスにマウントすることで、http://localhost:3001/adminでアクセスできるようになります。

この構造により、PrismaのモデルがそのままCRUD操作可能な管理画面として動作します。getModelByNameでモデルを取得し、propertiesで表示方法をカスタマイズする、というのがAdminJSの基本的な流れです。

便利なスクリプト設定

開発を楽にするため、package.jsonにスクリプトを追加しました:

{
  "scripts": {
    "dev": "next dev",
    "dev:admin": "cd admin && bun run dev",
    "dev:all": "bun run dev & bun run dev:admin"
  }
}

dev:allを使えば、Next.jsアプリと管理画面を同時に起動できます👍

リソース設定のポイント

AdminJSの真価は、リソース設定の柔軟性にあります。今回設定したTodoモデルを例に、重要なオプションを説明します。

フィールド設定の使い分け

properties: {
  title: {
    isRequired: true,  // 必須入力
    isTitle: true,     // リスト表示時のタイトル
  },
  description: {
    type: 'textarea',  // 複数行テキストエリア
  },
  completed: {
    type: 'boolean',   // チェックボックス
  },
}

isTitle: trueを設定することで、そのフィールドがレコードの代表的な表示名として使われます。typeの指定により、フォームの入力コンポーネントも自動的に適切なものが選ばれます。

表示制御の細かい設定

properties: {
  createdAt: {
    isVisible: { 
      list: true,     // リスト画面で表示
      filter: false,  // フィルターには含めない
      show: true,     // 詳細画面で表示
      edit: false     // 編集画面では非表示(編集不可)
    },
  },
}

この粒度の細かい制御ができるのが、AdminJSの良いところです。作成日時は見たいけど編集はさせたくない、といった要求に簡単に対応できます。

ブランディング設定

デフォルトのデザインでも十分使えますが、少しカスタマイズするだけで印象が変わります:

branding: {
  companyName: 'Todo App Admin Panel',
  withMadeWithLove: false,  // 「Made with ♥ by AdminJS」を非表示
  logo: false,
  theme: {
    colors: {
      primary100: '#4f46e5',  // Indigo色でブランディング
      primary80: '#6366f1',
      primary60: '#818cf8',
    },
  },
}

動作確認

実装が完了したら、まずは管理画面だけを起動して動作確認します:

cd app
bun run dev:admin

http://localhost:3001/adminにアクセスすると、自動生成された管理画面が表示されます。今回のプロジェクトでは、Todoが30件、Assigneeが5件のテストデータを事前に投入しており、以下の機能が正常に動作することを確認できました:

  • リスト表示とページネーション
  • フィルター機能(ID、タイトル、完了状態、担当者IDで絞り込み)
  • ソート機能(各カラムでの昇順・降順)
  • CRUD操作(作成、閲覧、更新、削除)

Assigneeリスト画面

実際の管理画面はこんな感じです。設定したブランディングカラーのIndigoが効いていて、シンプルながら見やすいUIになっています:

Assigneeリスト画面

左サイドバーのNavigationに「Database」グループとして、TodoとAssigneeが表示されています。各レコードには作成日時も表示され、右側の「...」メニューから編集・削除が可能です。

Todo編集画面

編集画面も自動生成されますが、設定した通りにフォームが構築されています:

Todo編集画面

Titleは必須入力、Descriptionはテキストエリア、Completedはチェックボックスとして表示されています。isVisibleの設定により、編集できないフィールド(IDや作成日時)は表示されません。

特に印象的だったのは、リレーションの表示です。TodoリストでAssigneeIdが表示され、詳細画面では関連するAssigneeの情報も確認できます。

遭遇したトラブルと解決策

App Routerとの非互換問題

最初はNext.js App RouterでAdminJSを動かそうと試行錯誤しましたが、公式ドキュメントを見ると「App Routerには対応していない」との記載がありました。

https://docs.adminjs.co/installation/plugins/nextjs

結果的に別ポートでExpressサーバーを立ち上げる方式にしましたが、これはこれで利点があります。管理画面とメインアプリケーションが完全に分離されるため、管理画面側で何か問題が起きてもメインアプリには影響しません。

tiptap依存関係エラー

前述のtiptapエラーは、実は結構厄介でした。エラーメッセージだけ見ても原因がわかりにくく、AdminJSのGitHubイシューやStack Overflowを調べて解決策を見つけました。

resolutionsフィールドを使った解決方法は、npmのworkspaceやyarnでも使える手法なので、類似の依存関係問題に遭遇した時にも応用できそうです。

セキュリティの注意点

今回の実装では開発環境として認証機能を省略していますが、本番環境では必ず認証を実装する必要があります。

AdminJSには @adminjs/passwordsというプラグインが用意されており、簡単な認証機能を追加できます:

import { buildAuthenticatedRouter } from '@adminjs/express'

const adminRouter = buildAuthenticatedRouter(admin, {
  authenticate: async (email, password) => {
    if (email === process.env.ADMIN_EMAIL && password === process.env.ADMIN_PASSWORD) {
      return { email }
    }
    return null
  },
  cookieName: 'admin-session',
  cookiePassword: process.env.COOKIE_SECRET,
})

本格運用では、さらにHTTPS必須、IPホワイトリスト、リバースプロキシ経由でのアクセスなど、複数のセキュリティレイヤーを重ねることを推奨します。

まとめ

AdminJSを使った管理画面実装、想像していた以上にスムーズでした。特に良かった点は:

  • Prisma統合の自然さ: 既存のスキーマから自動生成される管理画面は、追加の設定なしでも十分実用的
  • 設定の柔軟性: フィールド単位での表示制御や、バリデーション設定の細かさが秀逸
  • Express統合: Next.jsと分離することで、それぞれの責務が明確になった

今後の拡張案としては:

  • 認証機能の追加(Clerk統合を検討中)
  • CSV/JSONエクスポート機能
  • カスタムダッシュボードの実装
  • バルク操作機能

実装完了事項をまとめると:

  • ✅ AdminJS 7.5.0のインストールと設定
  • ✅ Prisma統合(Todo、Assigneeモデル)
  • ✅ Express サーバーの構築(port 3001)
  • ✅ tiptap依存関係問題の解決
  • ✅ フィルター・ソート・ページネーション機能
  • ✅ カスタムブランディング設定

管理画面の開発で時間を節約したい場合、AdminJSは検討する価値のある選択肢だと思います。特にPrismaを既に使っているプロジェクトなら、導入コストの低さは魅力的です✨

参考リンク:

株式会社StellarCreate | Tech blog📚

Discussion