React + NestJS + BetterAuth で認証機能を実装する
「BetterAuth」という認証ライブラリを見つけ、興味を持ったため、今回学習を兼ねて試してみました。
構成は、フロントエンドに React
、バックエンドに NestJS
を採用し、認証処理をバックエンドで担当する構成で進めます。
1. バックエンドのセットアップ
まずは、認証機能の根幹となるバックエンドから構築します。
1.1. Better Auth と Drizzle をインストール
npm install better-auth
npm i drizzle-orm pg dotenv
npm i -D drizzle-kit tsx @types/pg
環境変数に以下を追加します。
BETTER_AUTH_SECRET=
DATABASE_URL= # PostgreSQLの接続URLを設定
1.2. Better Auth と Drizzle の設定
認証状態をデータベースで永続化するため、Drizzle ORM と Better Auth のアダプターを設定します。
最初に、src/lib/auth.ts
を作成し、Better Auth の基本設定を記述します。
drizzleAdapter
を使用して、Drizzle と Better Auth を連携させます。
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db"; // Drizzleインスタンス
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // "mysql"や"sqlite"も選択可能
}),
});
次に、src/db/index.ts
を作成し、Drizzle のデータベース接続を初期化します。
ここでは PostgreSQL を使用します。
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
});
export const db = drizzle(pool);
1.3. NestJS プロジェクトの設定
NestJS で .
からのパスエイリアス(例: @/db
)を解決できるように、tsconfig.json
を修正します。
{
"compilerOptions": {
// ...既存設定...
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
また、Drizzle Kit を使用してマイグレーションを管理するためdrizzle.config.ts
をプロジェクトルートに作成します。
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
1.4. データベーススキーマの生成とマイグレーション
設定が完了したら、Better Auth が必要とするデータベーススキーマを生成し、マイグレーションを実行します。
まず、Better Auth の CLI を使って、認証に必要なテーブルスキーマを生成します。
npx @better-auth/cli@latest generate
このコマンドを実行すると、プロジェクトのルートに auth-schema.ts
というファイルが生成されます。
このファイルを schema.ts
にリネームし、backend/src/db/
ディレクトリへ移動させます。
次に、Drizzle Kit を使ってマイグレーションファイルを生成し、データベースに適用します。
# マイグレーションファイルを生成
npx drizzle-kit generate
# マイグレーションをデータベースに適用
npx drizzle-kit migrate
migrate
コマンドが成功すると、データベースに必要なテーブル(例: users
, sessions
)が作成されます。
1.5. NestJS への Better Auth 統合
公式の NestJS 統合ライブラリをインストールします。
npm install @thallesp/nestjs-better-auth
Better Auth はリクエストボディを内部で解析するため、NestJS のデフォルト bodyParser
を無効化する必要があ流ため、src/main.ts
を以下のように修正します。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bodyParser: false, // BetterAuthがリクエストボディを自前で処理するため無効化
});
await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();
最後に、AppModule
に AuthModule
をインポートして、アプリケーション全体で Better Auth が利用できるようにします。
import { Module } from '@nestjs/common';
import { AuthModule } from '@thallesp/nestjs-better-auth';
import { auth } from './lib/auth'; // 先ほど作成したauth設定
@Module({
imports: [AuthModule.forRoot({ auth })]
})
export class AppModule {}
2. 認証フローの実装
バックエンドのセットアップが完了したところで、実際にサインイン・サインアップ機能を実装していきます。
2.1. エラー①: Drizzle スキーマが見つからない
最初のサインイン試行で、以下のエラーが発生しました。
ERROR [Better Auth]: BetterAuthError: [# Drizzle Adapter]: The model "users" was not found in the schema object. Please pass the schema directly to the adapter options.
これは、Better Auth の Drizzle アダプターが、users
テーブルを含むデータベーススキーマを認識できていないことが原因です。
解決策:
src/db/schema.ts
からスキーマ定義をエクスポートし、src/lib/auth.ts
の drizzleAdapter
に明示的に渡すことで解決します。
// ... users, sessions, accounts, verifications の定義 ...
export const schema = { users, sessions, accounts, verifications }; // 末尾に追加
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { schema } from '../db/schema'; // スキーマをインポート
import { db } from '@/db';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema, // スキーマオブジェクトを渡す
usePlural: true,
}),
emailAndPassword: { enabled: true },
});
この修正により、ユーザーが存在しない場合に INVALID_EMAIL_OR_PASSWORD
エラーが返るようになり、アダプターが正しく動作していることを確認できました。
2.2. エラー②: サインアップ時のバリデーション
次にサインアップを試すと、パスワードに関するエラーが発生しました。
ERROR [Better Auth]: Password is too short
Better Auth にはデフォルトでパスワードの最小長が設定されています。
デフォルトで、8〜128文字までのパスワードが許可されており、minPasswordLength
maxPasswordLength
から変更が可能です。
自己サインアップを無効化したい場合は、
auth.ts
にdisableSignUp: true
を追加することで対応できます。
パスワードの組み合わせ(例:大文字・小文字・数字を必須にするなど)までは指定できないため、要件に応じて別途バリデーションを実装する必要がありそうです。
参考
リクエストボディに name
、email
、password
を含めて8文字以上のパスワードで再度実行することで、サインアップに成功しました。
{
"token": "...",
"user": {
"id": "...",
"email": "test@mail.example.test",
"name": "山田太郎",
...
}
}
重複したメールアドレスで登録しようとすると、USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL
エラーが正しく返ることも確認できました。サインイン、サインアウトも正常に動作します。
3. フロントエンドの構築 (React & Zustand)
バックエンドの準備が整ったので、次はクライアントサイドを実装します。
3.1. Better Auth クライアントと Zustand の導入
React 用の Better Auth クライアントと、状態管理ライブラリ Zustand をインストールします。
npm install better-auth zustand
src/lib/auth.ts
を作成し、バックエンド API と通信するためのクライアントを初期化します。
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_BASE_URL || "http://localhost:3000"
})
次に、認証状態(ユーザー情報、ローディング状態)と認証関連の関数(signIn
, signUp
, signOut
など)をグローバルに管理するための Zustand ストアを作成します。
authClient
が提供するメソッドの詳細は公式ドキュメントで確認できます。
import { create } from 'zustand';
import { authClient } from './auth';
// Userインターフェース定義...
interface AuthState {
user: User | null;
isLoading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string, name: string) => Promise<void>;
signOut: () => Promise<void>;
checkSession: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
isLoading: true,
signIn: async (email, password) => {
const { data, error } = await authClient.signIn.email({
email,
password,
rememberMe: true,
});
if (error) {
throw new Error(error.message);
}
if (data?.user) {
set({ user: data.user });
}
},
// signUp, signOut, checkSession の実装...
}));
4. フロントエンドとバックエンドの連携
作成したフロントエンドから実際にサインイン処理を実行し、連携上の問題を解決します。
4.1. エラー③: CORS エラー
サインインを実行すると、ブラウザのコンソールに CORS ポリシー違反のエラーが表示されました。
これは、異なるオリジンへのリクエストがサーバー側で許可されていないためです。
解決策:
NestJS 側で、特定のオリジンからのリクエストを許可する設定を追加します。
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bodyParser: false });
app.enableCors({
origin: 'http://localhost:5174', // フロントエンドのオリジンを許可
credentials: true, // Cookieを含むリクエストを許可
});
await app.listen(process.env.PORT ?? 3000);
}
4.2. エラー④: 信頼されていないオリジン (Trusted Origins)
CORSエラーを解決後、今度は Better Auth 側で Invalid callbackURL
エラーが発生しました。
INFO [Better Auth]: If it's a valid URL, please add http://localhost:5174/ to trustedOrigins in your auth config
これは、Better Auth が信頼できると判断したオリジンからのリクエストのみを受け付ける仕様になっているためです。
解決策:
バックエンドの src/lib/auth.ts
に、フロントエンドのオリジンを trustedOrigins
として追加します。
export const auth = betterAuth({
database: drizzleAdapter(db, {
// ...
}),
trustedOrigins: ['http://localhost:5174'], // 信頼するオリジンとして追加
});
5. 動作確認
すべての設定が完了し、React アプリケーションからサインイン、サインアップ、サインアウトが問題なく実行できることを確認できました。
サインアップ
サインイン
サインアウト
まとめ
BetterAuthを学習を兼ねて試してみましたが、非常にシンプルで簡単に認証機能を実装できました。
IdP(Identity Provider)との連携など、様々な認証方式を簡単に追加できる点が強力だと思っていますので、今後は、ソーシャルログインなど他の認証方式も試してみたいと思います。
作成物
Discussion