clerk-nextjs-patterns について
Clerk と Next.js を使う開発者向けに、認証、ルート保護、キャッシュ、トークン処理のパターンをまとめたスキルです。タスク別の資料で、現行 SDK と Core 2 の違いも説明します。
主な機能
- 01サーバーとクライアントの認証パターン
- 02ページ、ルート、アクションの保護
- 03Cache Components への対応
- 04JWT とセッショントークンの処理
- 05認証でよくある問題の診断
活用シーン
- 01Server Component と Client Component の認証 API を選ぶ
- 02ページや Server Action にアクセス制御を加える
- 03Cache Components 使用時のビルド問題に対処する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ clerk/ skills --skill clerk-nextjs-patternsインストール後、Claude Code で/clerk-nextjs-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/api-routes.mdcaching-auth.mdmiddleware-strategies.mdserver-actions.mdserver-vs-client.mdtemplates/nextjs-basic-auth/next.config.tspackage.jsonproxy.tstsconfig.jsontemplates/nextjs-basic-auth/app/layout.tsxpage.tsx詳細
Next.js のパターン
バージョン: SDK のバージョンは
package.jsonで確認してください。バージョン一覧はclerkスキルを参照してください。Core 2 との違いは、> **Core 2 ONLY (skip if current SDK):**の注記でインラインに示しています。
基本的なセットアップについては、clerk-setup スキルを参照してください。
ルート保護: 各ページ、Route Handler、Server Function で
await auth.protect()を呼び出してください。clerkMiddleware()はそのままにしてください。createRouteMatcherは@clerk/nextjs7 で非推奨となり、次のメジャーバージョンで削除されます。追加しないでください。プロジェクトですでに使用されている場合は、そのまま動作する状態にし、ユーザーに移行ガイドを案内してください。
必要な情報は?
| タスク | 参照先 |
|---|---|
サーバーとクライアントの認証(auth() とフック) | references/server-vs-client.md |
| ルートの保護(各ページ、ハンドラー、Server Function で確認) | references/middleware-strategies.md |
| Server Action の保護 | references/server-actions.md |
| API ルートの認証(401 と 403) | references/api-routes.md |
| 認証データのキャッシュ(ユーザー単位のキャッシュ) | references/caching-auth.md |
Cache Components での next build エラーの修正 | Cache Components |
参照先
| 参照先 | 説明 |
|---|---|
references/server-vs-client.md | await auth() とフックの比較 |
references/middleware-strategies.md | 各リソースでの auth.protect()、proxy.ts でそのまま使う clerkMiddleware()(Next.js <=15 の場合は middleware.ts) |
references/server-actions.md | 更新処理の保護 |
references/api-routes.md | 401 と 403 |
references/caching-auth.md | 'use cache' によるユーザー単位のキャッシュ |
基本的な考え方
サーバーとクライアントでは、認証 API が異なります。
- サーバー:
@clerk/nextjs/serverのawait auth()(非同期です!) - クライアント:
@clerk/nextjsのuseAuth()フック(同期です)
決して混在させないでください。Server Component ではサーバー用のインポートを使用し、Client Component ではフックを使用してください。
auth() の主なプロパティ:
isAuthenticated— ブール値。!!userIdパターンの代わりに使用しますsessionStatus—'active'|'pending'。未完了のセッションタスクを検出しますuserId、orgId、orgSlug、has()、protect()— 変更はありません
Core 2 ONLY (skip if current SDK):
isAuthenticatedとsessionStatusは使用できません。代わりに!!userIdを確認してください。
最小パターン
// Server Component
import { auth } from '@clerk/nextjs/server'
import { Suspense } from 'react'
async function Greeting() {
const { isAuthenticated, userId } = await auth() // MUST await!
if (!isAuthenticated) return <p>Not signed in</p>
return <p>Hello {userId}</p>
}
export default function Page() {
return (
<Suspense>
<Greeting />
</Suspense>
)
}
auth() はリクエストを読み取るため、ページ自体ではなく、<Suspense> 内の非同期子コンポーネントで実行します。Cache Componentsを参照してください。
Core 2 ONLY (skip if current SDK):
isAuthenticatedは使用できません。代わりにif (!userId)を使用してください。
<Show> による条件付きレンダリング
<Show> は認証状態に基づいて子要素をレンダリングします。認証チェックと認可(機能、プラン、ロール、権限)の両方を、1 つのコンポーネントで処理できます。Server Component では、サーバー上でリクエストからセッションを読み取るため、<Suspense> で囲んでください。Client Component では、クライアント側の認証状態を読み取るため、境界は不要です。
認証チェック:
import { Show } from '@clerk/nextjs'
import { Suspense } from 'react'
<Suspense>
<Show when="signed-in" fallback={<p>Please sign in</p>}>
<Dashboard />
</Show>
</Suspense>
認可チェック(B2B)(Server Component でレンダリングする場合も、同様に <Suspense> で囲んでください):
// Feature-based (preferred — features can move between plans without redeploy)
<Show when={{ feature: 'analytics' }} fallback={<UpgradePrompt />}>
<AnalyticsDashboard />
</Show>
// Permission-based (preferred over role-based for granular access)
<Show when={{ permission: 'org:invoices:create' }}>
<NewInvoiceButton />
</Show>
// Plan-based (tier-level gating)
<Show when={{ plan: 'pro' }}>
<ProFeatures />
</Show>
// Role-based (use sparingly — prefer permission)
<Show when={{ role: 'org:admin' }}>
<AdminPanel />
</Show>
複雑なロジックのコールバック:
<Show when={(has) => has({ role: 'org:admin' }) || has({ role: 'org:billing_manager' })}>
<BillingActions />
</Show>
Core 2 ONLY (skip if current SDK):
<Show>は存在しません。認証には<SignedIn>と<SignedOut>を使用してください。認可(ロール/権限)には、同じプロパティ名(role、permission、condition)を使って<Protect>を使用してください。機能およびプランに基づくバリエーションには Core 3 が必要です。完全な移行表については、clerk-custom-uiスキルのcore-3/show-component.mdを参照してください。
Cache Components
新しい Next.js アプリでは、next.config に cacheComponents: true が設定されます(Next.js 16.4 以降の create-next-app のデフォルト)。これを有効にすると、<Suspense> 境界の外でリクエストデータを読み取る場合、next build は失敗します。エラーは Next.js encountered uncached or runtime data during prerendering です。Clerk のレンダリングモードガイドにある Cache Components を参照してください。
- Server Component 内でレンダリングされる
<Show>と、auth()またはcurrentUser()を呼び出す Server Component は、認証に依存する UI にできるだけ近い位置で<Suspense>で囲んでください。Next.js がプリレンダリングできるよう、<ClerkProvider>と静的なレイアウトは境界の外に置いてください。 - レイアウトのヘッダーでは、
<header>内に<Suspense>を 1 つ配置し、その中でsigned-outとsigned-inの両方の<Show>ブロックを囲んでください。 - データをレンダリングするために
auth()またはcurrentUser()を呼び出すページでは、読み取り処理を<Suspense>内の非同期の子コンポーネントに移し、デフォルトエクスポートするページは同期処理のままにしてください。 - ページ全体のレンダリングを
auth.protect()、redirectToSignIn()、またはredirect()で制御する場合は、ページはそのままにして、隣にloading.tsxを追加してください。Read auth data inside Suspense を参照してください。 - キャッチオールルートの
<SignIn />、<SignUp />、<UserProfile />、<OrganizationProfile />ページは、<Suspense>で囲んでください。これらは Client Component 内でもusePathname()で URL を読み取ります。Prebuilt components on catch-all routes を参照してください。 'use cache'の中でauth()を呼び出さないでください。ID は外で読み取り、渡してください。references/caching-auth.mdと 'use cache' を使ってユーザーごとにデータをキャッシュする を参照してください。
失敗するルートをすべて一覧表示するには、next build --debug-prerender を使用してください。通常の next build は最初のエラーで停止します。
よくある落とし穴
| 症状 | 原因 | 修正方法 |
|---|---|---|
Server Component 内の userId が undefined | await が不足している | auth() ではなく await auth() を使用する |
| API ルートで認証が機能しない | matcher が不足している | proxy.ts に `'/(api |
next build が "uncached or runtime data during prerendering" で失敗する | <Suspense> の外に auth()、currentUser()、またはサーバー側の <Show> がある | 認証に依存する UI を <Suspense> で囲む(Cache Components を参照) |
| キャッシュから別のユーザーのデータが返される | userId がキャッシュキーに含まれていない | 'use cache' 関数に引数として userId を渡す |
| ミューテーションが認証を迂回する | Server Action が保護されていない | アクションの先頭で auth() を確認する |
| サインアウト中の保護されたページが 307 ではなく 200 を返す | ストリーミング開始後にリダイレクトされた(例:loading.tsx の下) | 本文がサインインページにリダイレクトし、保護されたデータを含まないことを確認する。チェックはページ内に残す |
| HTTP エラーコードが間違っている | 401 と 403 を混同している | 401 = サインインしていない、403 = 権限がない |
セッショントークンとカスタム JWT
外部 API 用の getToken()
Clerk ダッシュボードで定義した JWT テンプレートを使って、カスタム JWT をサードパーティサービス(Hasura、Supabase など)に渡します。
サーバー側(Server Component または Route Handler):
import { auth } from '@clerk/nextjs/server'
export async function ExternalData() {
const { getToken } = await auth()
const token = await getToken({ template: 'hasura' })
if (!token) return <p>Not authenticated</p>
const res = await fetch('https://api.example.com/graphql', {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
return <pre>{JSON.stringify(data)}</pre>
}
<ExternalData /> を <Suspense> 内でレンダリングしてください。
クライアント側(Client Component):
'use client'
import { useAuth } from '@clerk/nextjs'
export function DataFetcher() {
const { getToken } = useAuth()
async function fetchData() {
const token = await getToken({ template: 'supabase' })
if (!token) return
const res = await fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${token}` },
})
return res.json()
}
return <button onClick={fetchData}>Fetch</button>
}
ユーザーが認証されていない場合、getToken() は null を返します。使用前に必ず null チェックを行ってください。
セッションデータ用の useSession()
クライアントコンポーネントでセッションのメタデータにアクセスします。
'use client'
import { useSession } from '@clerk/nextjs'
export function SessionInfo() {
const { session } = useSession()
if (!session) return null
return (
<p>
Session {session.id} — last active: {session.lastActiveAt.toISOString()}
</p>
)
}
手動での JWT 検証(Clerk ミドルウェアなし)
Authorization ヘッダーまたは __session Cookie(同一オリジン)から Clerk のセッショントークンを受け取るスタンドアロン API サーバー向けです。
@clerk/backend の verifyToken を使用する場合(推奨):
import { verifyToken } from '@clerk/backend'
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return res.status(401).json({ error: 'No token' })
try {
const claims = await verifyToken(token, {
jwtKey: process.env.CLERK_JWT_KEY,
})
// claims.sub = userId
} catch {
return res.status(401).json({ error: 'Invalid token' })
}
jsonwebtoken を使用する場合(@clerk/backend を使用できない場合):
import jwt from 'jsonwebtoken'
const publicKey = process.env.CLERK_PEM_PUBLIC_KEY!.replace(/\\n/g, '\n')
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return res.status(401).json({ error: 'No token' })
try {
const claims = jwt.verify(token, publicKey, { algorithms: ['RS256'] }) as jwt.JwtPayload
// Manually check exp and nbf (jsonwebtoken does this automatically, but verify azp if needed)
// claims.sub = userId
} catch {
return res.status(401).json({ error: 'Invalid or expired token' })
}
トークンの取得元:
- 同一オリジンのリクエスト:
__sessionCookie(Clerk が自動的に設定します) - クロスオリジン/モバイル/API 間:
Authorization: Bearer <token>ヘッダー
CRITICAL:
expとnbfのクレームは必ず確認してください。@clerk/backendのverifyTokenはこの確認を自動的に処理しますが、生のjsonwebtokenを使用する場合は、ignoreExpiration: false(デフォルト)を設定し、clockToleranceを最小限にしてください。
関連項目
clerk-setup- Clerk の初期セットアップclerk-orgs- B2B パターン(アクティブな組織、ロール/権限によるアクセス制御)clerk-billing-has()を使ったプランと機能の利用権限clerk-webhooks- ユーザー/組織のイベントをデータベースに同期clerk-custom-ui- 組み込みコンポーネントのテーマ設定とカスタマイズ