Next.js Learnを読んで便利だと思ったもの一覧
12章
Object.fromEntries()
const { name, email, age } = Object.fromEntries(formData.entries());
このコードは以下の利点があります:
-
簡潔性: 一行で複数のフォームフィールドの値を取得できます。
-
分割代入の活用: JavaScriptの分割代入(Destructuring assignment)を使用して、オブジェクトから必要なプロパティだけを抽出しています。
-
型安全性の向上: 特定のフィールドだけを明示的に取り出すことで、予期しないフィールドの混入を防ぎます。
-
コードの可読性: どのフィールドを使用するかが一目で分かります。
使用例:
// フォームが送信されたとき
async function handleSubmit(formData) {
const { name, email, age } = Object.fromEntries(formData.entries());
console.log(name); // 例: 'John Doe'
console.log(email); // 例: 'john@example.com'
console.log(age); // 例: '30'
// ここで取得したデータを使って処理を行う
// 例: データベースに保存、APIにリクエストを送る、など
}
注意点:
- フォームにこれらのフィールド(name, email, age)が確実に存在することを前提としています。
- 全ての値は文字列として取得されます。数値や日付などは別途型変換が必要です。
- フォームに予期しないフィールドがある場合、それらは無視されます。
この方法は、特に処理したいフィールドが明確で、それ以外のフィールドは無視したい場合に非常に有用です。コードの意図が明確になり、後の処理も簡潔になります。
formData と Zodの組み合わせ
import { z } from 'zod';
// フォームデータのスキーマを定義
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
age: z.string().refine((val) => !isNaN(Number(val)), {
message: "Age must be a number"
}).transform(Number).pipe(z.number().positive().int().max(120)),
});
// フォームデータの型を推論
type FormData = z.infer<typeof formSchema>;
// フォーム送信時の処理
async function handleSubmit(formData: FormData) {
try {
// フォームデータをパースしてバリデーション
const { name, email, age } = Object.fromEntries(formData.entries());
const validatedData = formSchema.parse({ name, email, age });
// バリデーションが成功した場合の処理
console.log("Validated data:", validatedData);
// ここで API 呼び出しやデータベース操作などを行う
} catch (error) {
if (error instanceof z.ZodError) {
console.error("Validation failed:", error.errors);
// ここでエラーメッセージを表示するなどのエラーハンドリングを行う
} else {
console.error("An unexpected error occurred:", error);
}
}
}
// 使用例
const formData = new FormData();
formData.append('name', 'John Doe');
formData.append('email', 'john@example.com');
formData.append('age', '30');
handleSubmit(formData);
このコード例では、Zodを使用してフォームデータのバリデーションを行っています。主なポイントは以下の通りです:
-
Zodスキーマの定義:
-
formSchema
でフォームフィールドの型と制約を定義しています。 - 各フィールドに対して詳細なバリデーションルールを設定できます。
-
-
型の推論:
-
z.infer<typeof formSchema>
を使用して、スキーマから TypeScript の型を自動生成しています。
-
-
バリデーションの実行:
-
formSchema.parse()
メソッドを使用して、フォームデータをバリデーションします。 - バリデーションが成功すると、型安全なオブジェクトが返されます。
-
-
エラーハンドリング:
-
z.ZodError
をキャッチして、バリデーションエラーを適切に処理します。
-
このアプローチの利点:
- 型安全性: TypeScriptと連携して、コンパイル時の型チェックが可能です。
- 柔軟性: 複雑なバリデーションルールを簡潔に記述できます。
- エラーメッセージのカスタマイズ: 各フィールドにカスタムエラーメッセージを設定できます。
- データ変換: バリデーション時にデータ型の変換(例:文字列から数値へ)も行えます。
Zodを使用することで、フォームデータのバリデーションをより堅牢かつ型安全に行うことができます。これは特に大規模なアプリケーションや複雑なフォームを扱う際に非常に有用です。
一例
import { createClient } from '@supabase/supabase-js';
import { revalidatePath } from 'next/cache';
// 共通のバリデーション関数
const validatePokemonData = (data: { name?: string }) => {
const errors: { [key: string]: string } = {};
if (!data.name || typeof data.name !== 'string' || data.name.trim() === '') {
errors.name = 'Invalid or missing Pokemon name';
}
// 他のフィールドのバリデーションをここに追加
return errors;
};
// 共通のSupabase操作関数
const performSupabaseOperation = async (operation: 'insert' | 'update', data: any, id?: string) => {
const supabase = createClient();
let query = supabase.from('pokemons');
if (operation === 'insert') {
query = query.insert(data);
} else if (operation === 'update' && id) {
query = query.update(data).eq('id', id);
} else {
throw new Error('Invalid operation or missing ID for update');
}
const { data: result, error } = await query.select();
if (error) throw error;
return result;
};
// createPokemon 関数
export const createPokemon = async (form: FormData) => {
const user = await getUser();
if (!user) {
throw new Error('User not authenticated');
}
const name = form.get('name') as string;
const errors = validatePokemonData({ name });
if (Object.keys(errors).length > 0) {
throw new Error(Object.values(errors).join(', '));
}
try {
const data = await performSupabaseOperation('insert', {
name: name.trim(),
trainerId: user.id,
level: 1,
});
revalidatePath('/pokemon');
return data;
} catch (error) {
console.error('Error creating Pokemon:', error);
throw new Error('Failed to create Pokemon');
}
};
// updatePokemon 関数 (例)
export const updatePokemon = async (id: string, form: FormData) => {
// ユーザー認証やその他の前処理
const name = form.get('name') as string;
const errors = validatePokemonData({ name });
if (Object.keys(errors).length > 0) {
throw new Error(Object.values(errors).join(', '));
}
try {
const data = await performSupabaseOperation('update', { name: name.trim() }, id);
revalidatePath('/pokemon');
return data;
} catch (error) {
console.error('Error updating Pokemon:', error);
throw new Error('Failed to update Pokemon');
}
};
この共通化されたアプローチには以下の利点があります:
-
コードの再利用性:
validatePokemonData
とperformSupabaseOperation
関数を複数の操作で再利用できます。 -
一貫性: すべての操作で同じバリデーションロジックが適用されるため、データの一貫性が保たれます。
-
保守性の向上: バリデーションロジックの変更が1か所で済むため、保守が容易になります。
-
拡張性: 新しいフィールドやバリデーションルールを追加する際に、共通関数を修正するだけで済みます。
-
エラーハンドリングの統一: 共通のエラーハンドリング方法を使用することで、一貫したエラーメッセージを提供できます。
-
テストの容易さ: 共通関数を個別にテストできるため、ユニットテストが書きやすくなります。
この方法を採用することで、コードの品質が向上し、開発効率も上がります。新しい操作(例:削除機能)を追加する際も、既存の共通関数を活用できるため、迅速に実装できます。
必要に応じて、さらにTypeScriptの型定義を強化したり、より詳細なエラーハンドリングを追加したりすることも可能です。プロジェクトの規模や要件に応じて、このアプローチをカスタマイズしていくことをお勧めします。
Zodを使用したフォームデータバリデーション
1. はじめに
このレポートでは、TypeScriptプロジェクトにおいて、Zodライブラリを使用してフォームデータのバリデーションを行う方法について説明します。特に、インボイス作成フォームを例に、スキーマの定義からバリデーションの実行まで、詳細に解説します。
2. スキーマの定義
まず、フォームデータの構造に合わせてZodスキーマを定義します。
import { z } from 'zod';
const FormSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(),
status: z.enum(['pending', 'paid']),
date: z.string(),
});
const CreateInvoice = FormSchema.omit({ id: true, date: true });
ここでのポイント:
-
z.coerce.number()
: 文字列を数値に自動変換します。 -
z.enum(['pending', 'paid'])
: statusフィールドの値を制限します。 -
FormSchema.omit()
: 特定のフィールドを除外した新しいスキーマを作成します。
3. バリデーションの実行
定義したスキーマを使用して、フォームデータのバリデーションを行います。
export async function createInvoice(formData: FormData) {
try {
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// バリデーション成功後の処理(例:データベースへの保存)
} catch (error) {
if (error instanceof z.ZodError) {
// バリデーションエラーの処理
console.error(error.errors);
}
}
}
4. Zodのバリデーション処理の詳細
4.1 parse メソッドの動作
CreateInvoice.parse(rawFormData)
は以下のように動作します:
-
スキーマとデータの照合:
定義されたスキーマと入力データの構造を比較します。 -
データの変換とバリデーション:
-
customerId
: 文字列であることを確認 -
amount
: 文字列から数値へ変換し、数値であることを確認 -
status
: 'pending' または 'paid' のいずれかであることを確認
-
-
結果の生成:
- 成功時: 検証済みデータオブジェクトを返します。
- 失敗時:
ZodError
をスローします。
4.2 バリデーションエラーの処理
Zodのバリデーションエラーは ZodError
インスタンスとして捕捉できます。
try {
const validatedData = CreateInvoice.parse(rawFormData);
// 検証成功時の処理
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation failed:', error.errors);
// エラーハンドリング(例:ユーザーへのフィードバック表示)
}
}
4.3 特殊なケース
-
無効なステータス:
status
フィールドに 'pending' または 'paid' 以外の値が入力された場合、ZodはZodError
をスローします。 -
数値への変換失敗:
amount
フィールドに数値に変換できない文字列(例:'abc')が入力された場合もZodError
が発生します。
5. まとめ
Zodを使用したフォームデータのバリデーションは、以下の利点があります:
- 型安全性: TypeScriptと連携し、静的型チェックを提供します。
- 柔軟性: 複雑なバリデーションルールを簡潔に表現できます。
- エラーハンドリング: 詳細なエラー情報を提供し、適切なユーザーフィードバックが可能です。
- 自動型推論: バリデーション後のデータ型が自動的に推論されます。
この方法を採用することで、フロントエンドとバックエンドで一貫したバリデーションルールを維持し、データの整合性を確保することができます。
あとで読む
zod
で number
型のバリデーションを行う際、基本的な number
型を扱う方法と、そこに追加できるバリデーションメソッドについて説明します。
number
型の定義
基本的な const schema = z.number();
これは単純に数値型であることを確認するスキーマです。
数値型に対する追加のバリデーション
-
min
: 最小値を設定します。 -
max
: 最大値を設定します。 -
gt
(greater than): より大きい値を設定します(>)。 -
gte
(greater than or equal): 以上の値を設定します(>=)。 -
lt
(less than): より小さい値を設定します(<)。 -
lte
(less than or equal): 以下の値を設定します(<=)。
number
型のバリデーション
例: 以下に、これらのメソッドを組み合わせた例を示します。
const schema = z.number()
.min(1, { message: "The number must be at least 1." }) // 最小値は1
.max(100, { message: "The number must be at most 100." }) // 最大値は100
.int({ message: "The number must be an integer." }); // 整数であることを要求
このスキーマでは、次のようなバリデーションが行われます:
- 値が
1
以上である必要がある。 - 値が
100
以下である必要がある。 - 整数である必要がある。
このように、zod
を使うと、数値に対して細かいバリデーションを簡単に追加できます。また、エラーメッセージをカスタマイズすることで、ユーザーにわかりやすいフィードバックを提供することも可能です。
15章 認証認可
Supabaseを使用して、より詳細な認証と認可のチェックを実装し、様々なシナリオに対応できるようにします。以下に、ログイン状態、組織登録状態、組織の存在、不正アクセスなどをチェックする実装例を示します。
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const { data: { session } } = await supabase.auth.getSession()
const isLoggedIn = !!session
const path = req.nextUrl.pathname
// ログインチェック
if (path.startsWith('/dashboard') && !isLoggedIn) {
return NextResponse.redirect(new URL('/login', req.url))
}
// 組織関連のチェック(例:ダッシュボードへのアクセス)
if (path.startsWith('/dashboard') && isLoggedIn) {
const { data: userOrg } = await supabase
.from('user_organizations')
.select('organization_id')
.eq('user_id', session.user.id)
.single()
if (!userOrg) {
return NextResponse.redirect(new URL('/no-organization', req.url))
}
const { data: org } = await supabase
.from('organizations')
.select()
.eq('id', userOrg.organization_id)
.single()
if (!org) {
return NextResponse.redirect(new URL('/organization-not-found', req.url))
}
}
return res
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
// app/dashboard/layout.tsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const supabase = createServerComponentClient({ cookies })
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
redirect('/login')
}
// 組織チェック
const { data: userOrg } = await supabase
.from('user_organizations')
.select('organization_id')
.eq('user_id', session.user.id)
.single()
if (!userOrg) {
redirect('/no-organization')
}
const { data: org } = await supabase
.from('organizations')
.select()
.eq('id', userOrg.organization_id)
.single()
if (!org) {
redirect('/organization-not-found')
}
// ユーザーの権限チェック(例:管理者のみアクセス可能なページ)
const isAdminPage = location.pathname.startsWith('/dashboard/admin')
if (isAdminPage) {
const { data: userRole } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', session.user.id)
.single()
if (userRole?.role !== 'admin') {
redirect('/dashboard') // 一般ダッシュボードにリダイレクト
}
}
return <div>{children}</div>
}
// app/no-organization/page.tsx
export default function NoOrganizationPage() {
return (
<div>
<h1>No Organization</h1>
<p>You are not registered to any organization. Please contact an administrator.</p>
</div>
)
}
// app/organization-not-found/page.tsx
export default function OrganizationNotFoundPage() {
return (
<div>
<h1>Organization Not Found</h1>
<p>The organization you are registered to no longer exists. Please contact support.</p>
</div>
)
}
// utils/checkAccess.ts
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function checkAccess(requiredRole: string = 'user') {
const supabase = createServerComponentClient({ cookies })
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
redirect('/login')
}
const { data: userRole } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', session.user.id)
.single()
if (userRole?.role !== requiredRole) {
redirect('/unauthorized')
}
return session
}
// app/dashboard/admin/page.tsx
import { checkAccess } from '@/utils/checkAccess'
export default async function AdminPage() {
await checkAccess('admin')
return (
<div>
<h1>Admin Dashboard</h1>
{/* Admin content */}
</div>
)
}
この実装では、以下のシナリオに対応しています:
-
ログインしていない場合:
- ダッシュボードへのアクセス時に
/login
にリダイレクトします。
- ダッシュボードへのアクセス時に
-
組織に登録されていない場合:
-
/no-organization
ページにリダイレクトし、適切なメッセージを表示します。
-
-
組織が存在しない場合:
-
/organization-not-found
ページにリダイレクトし、適切なメッセージを表示します。
-
-
不正アクセス(権限がない場合):
- 管理者ページなど、特定の権限が必要なページへのアクセスを制限します。
- 権限がない場合は一般ダッシュボードや
/unauthorized
ページにリダイレクトします。
主な特徴:
- ミドルウェアでの事前チェック: 各リクエストで基本的な認証と組織チェックを行います。
- レイアウトコンポーネントでの詳細チェック: より詳細な権限チェックを行います。
- ユーティリティ関数 (
checkAccess
): 特定のページで必要な権限チェックを簡単に行えます。 - エラーページの実装: 各エラーケースに対応する専用ページを用意しています。
この実装により、様々な認証・認可シナリオに対応し、セキュアでユーザーフレンドリーな体験を提供できます。必要に応じて、さらにカスタマイズや機能追加を行うことができます。
Middele Wareの学習してる際の疑問
Q. リクエスト処理のフローに関して
App Routerは従来のPages Routerとは異なるアーキテクチャを採用しているため、リクエスト処理のフローも異なります。App Routerでのフローを図示し、主な違いを説明しましょう。
App Routerでのリクエスト処理フローは以下のようになります:
-
Incoming Request: ユーザーからのリクエストがサーバーに到達します。
-
Middleware: Pages Routerと同様に、すべてのリクエストは最初にMiddlewareを通過します。
-
Route Handler: App Routerでは、ルートハンドラがリクエストを処理します。
-
Route Type: ページルートかAPIルートかを判断します。
-
Server Components Execution (ページルートの場合):
- サーバーコンポーネントが実行されます。
- データフェッチはここで行われます(getServerSidePropsの代わり)。
-
Server Components Rendering:
- サーバーコンポーネントがレンダリングされます。
-
HTML Stream Sent:
- 生成されたHTMLがストリームとしてクライアントに送信されます。
-
Client Components Execution:
- クライアントコンポーネントがブラウザで実行されます。
-
Client Components Hydration:
- クライアントコンポーネントが水和(ハイドレーション)されます。
主な違いと特徴:
-
Server Components: App Routerの最大の特徴です。サーバーサイドでレンダリングされ、HTMLとして送信されます。
-
Streaming: HTMLをストリームとして送信するため、ユーザーはより早くコンテンツを見ることができます。
-
データフェッチ: getServerSidePropsの代わりに、サーバーコンポーネント内で直接データフェッチを行います。
-
クライアントサイドナビゲーション: ページ間の遷移がよりスムーズになります。
-
ルート定義: ファイルシステムベースのルーティングがより直感的になりました。
-
レイアウト: ネストされたレイアウトの実装が容易になりました。
App RouterはNext.jsの将来の方向性を示すものですが、従来のPages Routerも引き続きサポートされています。プロジェクトの要件に応じて、適切なルーターを選択することが重要です。
App Routerでのセキュアな実装方法を説明します。App Routerでは、サーバーコンポーネントとクライアントコンポーネントの概念が重要になります。
- ルート保護
App Routerでは、ミドルウェアを使用してルート全体を保護できます。
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const session = request.cookies.get('session')
if (!session && !request.nextUrl.pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
}
- サーバーコンポーネントでのデータフェッチ
App Routerでは、サーバーコンポーネント内で直接データをフェッチでき、より安全にセンシティブな操作を行えます。
// app/dashboard/page.tsx
import { getServerSession } from "next-auth/next"
import { redirect } from 'next/navigation'
async function getDashboardData(token: string) {
const res = await fetch('https://api.example.com/dashboard-data', {
headers: {
'Authorization': `Bearer ${token}`
},
cache: 'no-store' // キャッシュを無効化
})
if (!res.ok) throw new Error('Failed to fetch dashboard data')
return res.json()
}
export default async function DashboardPage() {
const session = await getServerSession()
if (!session) redirect('/login')
const dashboardData = await getDashboardData(session.accessToken)
return (
<div>
<h1>Dashboard</h1>
{/* dashboardDataを使用してUIをレンダリング */}
</div>
)
}
このサーバーコンポーネントでは:
- セッションチェックを行い、未認証ユーザーをリダイレクトします。
- 認証トークンを使用して安全にデータをフェッチします。
-
cache: 'no-store'
オプションでキャッシュを無効化しています。
- クライアントコンポーネントでの状態管理
センシティブな情報を扱う場合、クライアントコンポーネントでの状態管理にも注意が必要です。
'use client'
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/react'
export default function SensitiveDataComponent() {
const { data: session } = useSession()
const [sensitiveData, setSensitiveData] = useState(null)
useEffect(() => {
if (session) {
fetchSensitiveData()
}
}, [session])
async function fetchSensitiveData() {
const res = await fetch('/api/sensitive-data', {
headers: {
'Authorization': `Bearer ${session.accessToken}`
}
})
if (res.ok) {
const data = await res.json()
setSensitiveData(data)
}
}
if (!session) return <div>Please log in to view this content</div>
return (
<div>
{/* センシティブなデータを表示 */}
</div>
)
}
- API Routesの利用
App Routerでも、必要に応じてAPI Routesを使用できます。これらはapp/api
ディレクトリに配置します。
// app/api/sensitive-data/route.ts
import { getServerSession } from "next-auth/next"
import { NextResponse } from 'next/server'
export async function GET() {
const session = await getServerSession()
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// ユーザーの権限チェック
if (!hasPermission(session.user, 'read:sensitive-data')) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
// センシティブなデータを取得
const sensitiveData = await fetchSensitiveData(session.user.id)
return NextResponse.json(sensitiveData)
}
- キャッシュコントロール
App Routerでは、ルートセグメントごとにキャッシュ動作を制御できます。
// app/dashboard/layout.tsx
export const revalidate = 0 // このセグメント以下のすべてのルートで動的レンダリングを強制
export default function DashboardLayout({ children }) {
return <div>{children}</div>
}
セキュリティのベストプラクティス:
- サーバーコンポーネントを活用:センシティブな操作はサーバーコンポーネントで行います。
- 適切な認証・認可:各リクエストで適切な認証・認可チェックを行います。
- キャッシュ制御:センシティブなデータには
cache: 'no-store'
やrevalidate = 0
を使用します。 - エラーハンドリング:適切なエラーハンドリングと、ユーザーフレンドリーなエラーメッセージを提供します。
- HTTPS:すべての通信でHTTPSを使用します。
- 最小権限の原則:必要最小限の権限のみを付与します。
App Routerを使用する場合、これらの方法を組み合わせることで、セキュアでパフォーマンスの高いアプリケーションを構築できます。特に、サーバーコンポーネントを効果的に活用することで、セキュリティを強化しつつ、優れたユーザー体験を提供することができます。
Middlewareに関して
Next.js のミドルウェアとリクエスト処理の流れについて、詳細に説明します。
- ミドルウェアの実行順序
Next.js でリクエストが処理される際、以下の順序で各要素が実行されます:
- ミドルウェアのマッチング
ミドルウェアはデフォルトですべてのルートで実行されますが、特定のパスに対してのみ実行するよう制限することができます。これには2つの方法があります:
a. カスタムマッチャー設定
middleware.js
ファイル内で config
エクスポートを使用して、ミドルウェアを適用するパスを指定できます。
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
export function middleware(request) {
// ミドルウェアのロジック
}
この例では、/about
と /dashboard
で始まるすべてのパスに対してミドルウェアが実行されます。
b. 条件文
ミドルウェア関数内で条件文を使用して、特定のパスに対してのみロジックを実行することもできます。
export function middleware(request) {
if (request.nextUrl.pathname.startsWith('/api/')) {
// API ルートに対する処理
} else if (request.nextUrl.pathname.startsWith('/dashboard/')) {
// ダッシュボードルートに対する処理
}
}
- ミドルウェアの使用例
ミドルウェアは様々な用途に使用できます。以下にいくつかの例を示します:
a. 認証チェック
import { NextResponse } from 'next/server'
export function middleware(request) {
const authToken = request.cookies.get('authToken')
if (!authToken && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
b. ヘッダーの追加
import { NextResponse } from 'next/server'
export function middleware(request) {
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
return response
}
c. A/Bテスト
import { NextResponse } from 'next/server'
export function middleware(request) {
if (request.nextUrl.pathname === '/') {
const bucket = Math.random() < 0.5 ? 'A' : 'B'
const response = NextResponse.next()
response.cookies.set('ab-test', bucket)
return response
}
}
ミドルウェアを使用することで、リクエスト処理の早い段階でカスタムロジックを適用できます。これにより、認証、リダイレクト、ヘッダー操作など、アプリケーション全体に影響を与える処理を効率的に実装できます。
ただし、ミドルウェアはすべてのリクエストに対して実行される可能性があるため、パフォーマンスに注意を払う必要があります。必要最小限の処理のみをミドルウェアで行い、可能な限り特定のルートに限定して使用することをお勧めします。
- マッチャー(Matcher)の詳細
マッチャーは、ミドルウェアを特定のパスに対してのみ実行するための強力なツールです。
a. 基本的な使用方法
// middleware.js
export const config = {
matcher: '/about/:path*',
}
この設定では、/about
で始まるすべてのパスに対してミドルウェアが実行されます。
b. 複数パスのマッチング
// middleware.js
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
この設定では、/about
と/dashboard
で始まるパスに対してミドルウェアが実行されます。
c. 正規表現を使用した高度なマッチング
マッチャーは完全な正規表現をサポートしているため、複雑なマッチングパターンを定義できます。
// middleware.js
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
この例では、特定のパス(API routes、静的ファイル、画像最適化ファイル、favicon)を除くすべてのリクエストパスにマッチします。
- 条件文の使用
条件文を使用すると、リクエストの特性に基づいてより細かい制御が可能になります。
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
// 追加の条件を設定可能
if (request.nextUrl.pathname.startsWith('/blog')) {
const authToken = request.cookies.get('authToken')
if (!authToken) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
}
この例では:
-
/about
で始まるパスを/about-2
にリライトします。 -
/dashboard
で始まるパスを/dashboard/user
にリライトします。 -
/blog
で始まるパスに対して認証チェックを行い、認証されていない場合はログインページにリダイレクトします。
- マッチャーと条件文の組み合わせ
マッチャーと条件文を組み合わせることで、より効率的で柔軟なミドルウェアを実装できます。
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/dashboard')) {
const authToken = request.cookies.get('authToken')
if (!authToken) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
if (request.nextUrl.pathname.startsWith('/api')) {
const apiKey = request.headers.get('x-api-key')
if (!apiKey) {
return new NextResponse(
JSON.stringify({ error: 'Missing API key' }),
{ status: 401, headers: { 'content-type': 'application/json' } }
)
}
}
}
この例では:
- マッチャーを使用して
/dashboard
と/api
で始まるパスのみを対象としています。 - 条件文を使用して、それぞれのパスタイプに応じた異なる処理を行っています。
マッチャーと条件文を適切に組み合わせることで、効率的でセキュアなミドルウェアを実装できます。これにより、アプリケーション全体のパフォーマンスを維持しつつ、必要な箇所で適切な処理を行うことが可能になります。
これらの機能を活用することで、認証、ルーティング、APIキー検証、A/Bテスティングなど、様々なユースケースに対応できます。
Next.js Middleware: 初心者のための完全ガイド
Next.jsは、Reactベースのウェブアプリケーションフレームワークとして人気を集めています。その中でも、Middlewareは非常に強力な機能の一つです。この記事では、Next.jsのMiddlewareについて、初心者の方にも分かりやすく解説していきます。
Middlewareとは?
Middlewareは、リクエストが完了する前にコードを実行できる機能です。つまり、ページやAPIルートが処理される前に、リクエストに基づいてレスポンスを変更したり、リダイレクトしたり、ヘッダーを修正したりすることができます。
簡単に言えば、Middlewareは「リクエストとレスポンスの間に立つ仲介者」のようなものです。
Middlewareのユースケース
Middlewareは様々な場面で活用できます。以下に主なユースケースを挙げてみましょう:
- 認証と認可: ユーザーのアイデンティティを確認し、特定のページやAPIルートへのアクセスを制御します。
- サーバーサイドリダイレクト: 特定の条件に基づいてユーザーを別のURLにリダイレクトします。
- パスの書き換え: A/Bテストやフィーチャーロールアウトのために、動的にパスを書き換えます。
- ボット検出: ボットトラフィックを検出してブロックし、リソースを保護します。
- ロギングと分析: ページやAPIの処理前にリクエストデータを取得し、分析します。
- フィーチャーフラグ: 特定の機能を動的に有効/無効にします。
Middlewareの基本的な使い方
Middlewareを使用するには、プロジェクトのルートディレクトリ(またはsrc
ディレクトリ)にmiddleware.ts
(または.js
)ファイルを作成します。
以下は、基本的なMiddlewareの例です:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// リクエストのURLをチェック
if (request.nextUrl.pathname.startsWith('/about')) {
// '/about'で始まるパスの場合、'/about-2'にリライト
return NextResponse.rewrite(new URL('/about-2', request.url))
}
// それ以外の場合は、リクエストを通常通り処理
return NextResponse.next()
}
この例では、/about
で始まるパスへのリクエストを/about-2
にリライトしています。
Middlewareの実行順序
Middlewareは、Next.jsの他の機能と組み合わせて使用されます。以下は、リクエスト処理の大まかな流れです:
-
next.config.js
のheaders -
next.config.js
のredirects - Middleware(リライト、リダイレクトなど)
-
next.config.js
のbeforeFiles(リライト) - ファイルシステムのルート(public/, _next/static/, pages/, app/など)
-
next.config.js
のafterFiles(リライト) - 動的ルート(/blog/[slug])
-
next.config.js
のfallback(リライト)
パスのマッチング
Middlewareをすべてのルートで実行すると、パフォーマンスに影響を与える可能性があります。そのため、特定のパスに対してのみMiddlewareを実行するように設定することができます。
matcherの使用
matcher
設定を使用すると、Middlewareを実行するパスを指定できます:
export const config = {
matcher: '/about/:path*',
}
この例では、/about
とそのサブパスに対してのみMiddlewareが実行されます。
複数のパスを指定することも可能です:
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
条件文の使用
matcher
の代わりに、条件文を使用してパスをフィルタリングすることもできます:
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
// '/about'で始まるパスの処理
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
// '/dashboard'で始まるパスの処理
}
}
NextResponseの活用
NextResponse
クラスを使用すると、以下のようなことが可能です:
- リダイレクト
- リライト
- レスポンスの生成
- ヘッダーの設定
- クッキーの操作
リダイレクトの例
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// '/old-page'へのアクセスを'/new-page'にリダイレクト
if (request.nextUrl.pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url))
}
}
ヘッダーの設定例
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// レスポンスヘッダーにカスタムヘッダーを追加
const response = NextResponse.next()
response.headers.set('x-custom-header', 'hello from middleware')
return response
}
クッキーの操作
Middlewareを使用して、リクエストとレスポンスのクッキーを簡単に操作できます:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// リクエストからクッキーを取得
let token = request.cookies.get('token')
// レスポンスにクッキーを設定
const response = NextResponse.next()
response.cookies.set('visited', 'true')
return response
}
CORSの設定
Cross-Origin Resource Sharing (CORS)の設定もMiddlewareで行うことができます:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const allowedOrigins = ['https://example.com', 'https://www.example.com']
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin')
if (origin && allowedOrigins.includes(origin)) {
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', origin)
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
return response
}
}
まとめ
Next.jsのMiddlewareは、Webアプリケーションの柔軟性と機能性を大幅に向上させる強力なツールです。認証、リダイレクト、ヘッダー操作など、様々なユースケースに対応できます。
以下は、Middlewareの主な特徴をまとめた表です:
特徴 | 説明 |
---|---|
実行タイミング | リクエスト処理の早い段階で実行される |
適用範囲 | 全てのルート、または指定したパスのみ |
主な機能 | リライト、リダイレクト、ヘッダー操作、レスポンス生成 |
ファイル名 |
middleware.ts またはmiddleware.js
|
配置場所 | プロジェクトのルートまたはsrc ディレクトリ |
Middlewareを効果的に使用することで、よりセキュアで柔軟なWebアプリケーションを構築することができます。ただし、パフォーマンスへの影響を考慮し、必要な場合にのみ使用することをお勧めします。
参考リソース
この記事を通じて、Next.jsのMiddlewareについての理解が深まったことを願っています。実際にプロジェクトで使用して、その威力を体験してみてください!
Next.js Middleware: 初心者のための完全ガイド(認証編)
認証によるアクセス制御
アプリケーションのセキュリティを確保するために、認証されたユーザーのみが特定の情報にアクセスできるようにすることは非常に重要です。Next.jsのMiddlewareを使用すると、この要件を効率的に実装できます。
基本的なアプローチ
- ユーザーの認証状態を確認する
- 認証されていない場合、ログインページにリダイレクトする
- 認証されている場合、リクエストを通常通り処理する
実装例
以下は、Middlewareを使用して未認証ユーザーのアクセスを制限する基本的な実装例です:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 認証トークンの取得(ここではクッキーを使用)
const token = request.cookies.get('auth_token')
// 保護されたルートのリスト
const protectedRoutes = ['/dashboard', '/profile', '/settings']
// 現在のパスが保護されたルートかチェック
const isProtectedRoute = protectedRoutes.some(route =>
request.nextUrl.pathname.startsWith(route)
)
// 保護されたルートで、かつトークンがない場合
if (isProtectedRoute && !token) {
// ログインページにリダイレクト
return NextResponse.redirect(new URL('/login', request.url))
}
// それ以外の場合は、リクエストを通常通り処理
return NextResponse.next()
}
// Middlewareを適用するパスを指定
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*'],
}
この例では以下のことを行っています:
- リクエストのクッキーから認証トークンを取得します。
- 保護したいルートのリストを定義します。
- 現在のリクエストが保護されたルートに対するものかチェックします。
- 保護されたルートへのアクセスで、かつ認証トークンがない場合、ログインページにリダイレクトします。
- それ以外の場合は、リクエストを通常通り処理します。
注意点
- この例では簡単のため、クッキーの存在のみをチェックしています。実際の実装では、トークンの有効性も検証する必要があります。
- セキュリティを強化するために、HTTPSを使用し、クッキーに
Secure
とHttpOnly
フラグを設定することをお勧めします。 - JWTなどのより高度な認証メカニズムを使用することで、さらにセキュリティを向上させることができます。
より高度な実装
より堅牢な認証システムを実装する場合、以下のような機能を追加することを検討してください:
-
トークンの検証: 単にトークンの存在をチェックするだけでなく、その有効性も確認します。
-
ロールベースのアクセス制御: ユーザーの役割に基づいて、特定のルートへのアクセスを制御します。
-
セッション管理: ユーザーセッションの有効期限を管理し、必要に応じて再認証を要求します。
以下は、これらの機能を取り入れたより高度な実装例です:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyToken } from './lib/auth' // トークン検証用の関数(実装が必要)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth_token')
// ルートとそれに必要な最小権限のマッピング
const routePermissions = {
'/dashboard': 'user',
'/admin': 'admin',
'/settings': 'user',
}
// 現在のパスに必要な権限を取得
const requiredPermission = Object.entries(routePermissions).find(([route]) =>
request.nextUrl.pathname.startsWith(route)
)?.[1]
// 権限が必要なルートの場合
if (requiredPermission) {
// トークンがない場合
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
// トークンを検証し、ユーザー情報を取得
const user = await verifyToken(token)
// ユーザーの権限が不足している場合
if (requiredPermission === 'admin' && user.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// 認証OK、リクエストを続行
return NextResponse.next()
} catch (error) {
// トークンが無効な場合
return NextResponse.redirect(new URL('/login', request.url))
}
}
// 認証が不要なルートの場合、リクエストを続行
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*', '/settings/:path*'],
}
この高度な実装では:
- ルートごとに必要な最小権限を定義しています。
- トークンの存在だけでなく、その有効性も検証しています。
- ユーザーの役割に基づいてアクセスを制御しています。
- 無効なトークンや不十分な権限の場合、適切にリダイレクトしています。
まとめ
Next.jsのMiddlewareを使用することで、アプリケーション全体で一貫した認証とアクセス制御を実装できます。この方法は:
- 集中管理:認証ロジックを一箇所で管理できます。
- 柔軟性:様々な認証戦略に適応できます。
- パフォーマンス:サーバーサイドで動作するため、効率的です。
ただし、Middlewareはエッジ環境で実行されるため、データベースへの直接アクセスなど、一部の操作が制限される可能性があります。そのような場合は、別のサービスと連携するなどの工夫が必要になるかもしれません。
認証は重要なセキュリティ機能であり、適切に実装することが非常に重要です。必要に応じて、専門家のアドバイスを求めたり、十分にテストを行うことをお勧めします。