Nuxt

Nuxt 徹底解説 — アーキテクチャと機能の全容

1. はじめに — Nuxt とは何か

Nuxt(ナクスト)は、Vue.js 上に構築されたフルスタック Web アプリケーションフレームワークである。React 界における Next.js、Svelte 界における SvelteKit と同じ立ち位置にあり、Vue 単体では難しい「サーバーサイドレンダリング (SSR)」「静的サイト生成 (SSG)」「ハイブリッドレンダリング」「サーバーエンドポイント」「ファイルベースのルーティング」「モジュールエコシステム」などを、設定ゼロに近い形で提供する。

ひとことで言えば、Nuxt は「Vue の上に薄く敷かれたフレームワーク」ではなく、Vite + Vue + Nitro + H3 + unjs エコシステム を統合した、極めて生産性の高い メタフレームワーク である。開発者は Vue のコンポーネントを書くだけで、世界中のさまざまなランタイム(Node.js、Cloudflare Workers、Deno Deploy、AWS Lambda、Vercel、Netlify、Bun、静的ホスティング)に自動的にデプロイできる成果物を得られる。

1.1 歴史的背景と現在位置

Nuxt は 2016 年、Chopin Brothers(Sébastien と Alexandre)によって Next.js に触発される形で始まった。Vue 2 時代の Nuxt 2 は、Webpack ベース・Node/Connect ベースのサーバーを中心にした構成で、Vue エコシステム標準の SSR ソリューションとして広く採用された。

2022 年末にリリースされた Nuxt 3 で大規模なアーキテクチャ刷新が行われた。主な変更点は次の通りである。

  • Vue 3 Composition API と <script setup> の前提化
  • Vite をデフォルトビルドツールとして採用(Webpack も選択可)
  • Nitro という独自サーバーエンジンの導入により、デプロイターゲットを抽象化
  • unjs エコシステム(unstorage, ofetch, h3, listhen, unimport, c12 など)への移行
  • 完全な TypeScript ファーストへの刷新
  • モジュール解決と自動インポート(Auto-imports)の強化

2024 年以降の Nuxt 3.x 系では、サーバーコンポーネント、Server Components Islands、View Transitions API 統合、Hybrid Rendering(ルートごとの描画方式選択)、useFetch の再設計、app.config.ts の安定化など、実戦向け機能が次々に投入された。そして 2025 年には Nuxt 4 が正式リリースされ、新しい app/ ディレクトリ構造、データフェッチのレイヤー分離、より厳密な型安全な DX が標準となっている。本稿では Nuxt 3 後期〜Nuxt 4 を主対象とするが、Nuxt 3 固有の注意点についても随時触れる。

1.2 Nuxt が解決する問題

素の Vue でアプリを組む場合、以下の課題を開発者自身が設計・組立しなければならない。

  1. ルーティング — Vue Router の設定、ネストルート、動的ルート、権限ガード
  2. SSR / SSGrenderToString、ハイドレーション、ストア転送、ストリーミング
  3. データ取得の一元管理 — サーバー/クライアント両方で動き、キャッシュ・再検証・エラー処理が効くもの
  4. ビルド・バンドル最適化 — コード分割、プリフェッチ、モジュール連結
  5. API バックエンド — 軽量なサーバーエンドポイント(通常 Express や Fastify を別立てで用意)
  6. デプロイ — プラットフォーム別のアダプタ、エッジ実行、静的書き出し
  7. SEO / メタタグ — OGP、構造化データ、ソーシャル対応
  8. 開発者体験 (DX) — Auto-imports、型生成、DevTools、HMR、ESLint

Nuxt はこれら全てを「既定でそこそこ正しく動く」状態にした上で、必要に応じて置き換え可能にしている。したがって小規模なブログから、エッジで動く大型 EC サイト、管理画面、SaaS まで幅広くカバーできる。

1.3 本稿の射程

本稿は Nuxt の全体像を理解することを目的に、A4 30 枚程度の分量で、以下を順に扱う。

  • アーキテクチャ(Vue, Vite, Nitro, H3, unjs)
  • nuxt.config.ts を中心とした設定の詳細
  • ディレクトリ構造と規約(app/, pages/, components/, server/, composables/ など)
  • レンダリングモード(SSR / SSG / SPA / ISR / Hybrid)の使い分けと設定例
  • ファイルベースルーティングと動的ルート
  • データ取得 (useFetch, useAsyncData, $fetch) と SSR でのキャッシュ戦略
  • 状態管理 (useState / Pinia)
  • Nitro サーバーエンジンと server/ ディレクトリによる API 実装
  • 自動インポート、コンポーザブル、ユーティリティ
  • プラグイン、ミドルウェア、レイアウト、エラーハンドリング
  • モジュールシステム(公式、コミュニティ)
  • TypeScript 連携と型生成
  • パフォーマンス最適化(Payload Extraction, Route Rules, ISR, Image, Font)
  • デプロイターゲット(Node, Vercel, Cloudflare, Deno, 静的)
  • Nuxt DevTools と開発者体験
  • テスト戦略(Vitest, Playwright, @nuxt/test-utils
  • よくある落とし穴とベストプラクティス

実際のプロジェクトで遭遇する設計判断に役立つよう、設定例・コード例・落とし穴を随所に織り込む。単なる API リファレンスではなく、「なぜそうなっているのか」を意識して読んでいただきたい。

2. アーキテクチャの全体像

Nuxt のアーキテクチャを一枚で表すと、次のレイヤー構造になる。

┌──────────────────────────────────────────────────────────────┐
│  Application Code (pages/, components/, layouts/, app/)      │
│  ├── Vue 3 (Composition API, <script setup>)                  │
│  └── Composables (useFetch, useAsyncData, useState, ...)      │
├──────────────────────────────────────────────────────────────┤
│  Nuxt Runtime                                                 │
│  ├── Vue Router (ファイルベースで自動生成)                      │
│  ├── Auto-imports (unimport)                                  │
│  ├── Hydration / Universal Rendering                          │
│  └── Payload / useState sync                                  │
├──────────────────────────────────────────────────────────────┤
│  Build Layer                                                  │
│  ├── Vite (開発時 HMR, 本番バンドル)                            │
│  └── Rollup (Nitro による server バンドル)                      │
├──────────────────────────────────────────────────────────────┤
│  Nitro Server Engine                                          │
│  ├── H3 (軽量 HTTP フレームワーク)                              │
│  ├── unstorage (KV 抽象)                                       │
│  ├── ofetch ($fetch)                                          │
│  ├── Route Rules (SSR/ISR/SPA/Prerender 切り替え)              │
│  └── Preset (Node, Vercel, Cloudflare, Deno, static, ...)     │
├──────────────────────────────────────────────────────────────┤
│  Runtime Target                                               │
│  └── Node.js / Edge (Workers) / Serverless / Static files     │
└──────────────────────────────────────────────────────────────┘

2.1 Vue 3 と Composition API

Nuxt 3 以降は Vue 3 の Composition API<script setup> を前提としており、Options API で書くことも可能だが、Auto-imports やコンポーザブルとの相性を考えると事実上 Composition API が標準である。

<!-- pages/index.vue -->
<script setup lang="ts">
const { data: posts, pending, error } = await useFetch('/api/posts')

useHead({
  title: 'ホーム',
  meta: [{ name: 'description', content: '最新記事一覧' }]
})
</script>

<template>
  <main>
    <h1>最新の記事</h1>
    <p v-if="pending">読み込み中...</p>
    <p v-else-if="error">エラー: {{ error.message }}</p>
    <ul v-else>
      <li v-for="post in posts" :key="post.id">
        <NuxtLink :to="`/posts/${post.id}`">{{ post.title }}</NuxtLink>
      </li>
    </ul>
  </main>
</template>

ここで注目すべきは、useFetch, useHead, NuxtLink のいずれも import 文が一切存在しない点である。Nuxt の Auto-imports が、コンポーザブル・コンポーネント・ユーティリティをコンパイル時にスキャンし、使用箇所に自動で import を差し込む。後述するが、これは単なる利便性ではなく、Tree-shaking と Type Safety を両立するための重要な基盤である。

2.2 Vite によるビルド層

Nuxt 3 以降のデフォルトビルダーは Vite である。dev 時は ESM ネイティブ配信 + HMR、build 時は Rollup を用いてサーバー・クライアント両方のバンドルを生成する。Webpack をどうしても使いたい場合は builder: 'webpack' を指定できるが、エコシステムは Vite に集中しており、特段の理由がない限り Vite を使うべきである。

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      target: 'esnext',
      cssMinify: 'lightningcss'
    },
    optimizeDeps: {
      include: ['lodash-es']
    }
  }
})

vite キー配下の設定はそのまま Vite に委譲されるため、既存 Vite プロジェクトの知識がそのまま活きる。

2.3 Nitro サーバーエンジン

Nuxt の最大の発明とも言えるのが Nitro である。Nitro は Nuxt のサーバー側部分(SSR、API ルート、ミドルウェア)を担う独立したサーバーエンジンで、一つのコードベースから、異なるプラットフォーム向けの成果物を自動ビルドできる。

  • Node.js サーバー (node-server, 既定)
  • Vercel(Functions / Edge / ISR / 静的)
  • Netlify(Functions / Edge)
  • Cloudflare Pages / Workers
  • Deno Deploy
  • AWS Lambda / Lambda@Edge
  • Azure Static Web Apps / Functions
  • Firebase Functions / Hosting
  • Static(完全静的書き出し)
  • Bun

NITRO_PRESET=vercel nuxt build のように環境変数で切り替えるだけで、同じアプリが全く異なるランタイムで動作する。

Nitro の内部は Rollup によってサーバーバンドルが生成される。クライアント側は Vite が担当し、サーバー側は Nitro (Rollup) が担当するという二層構成である。

2.4 H3 と unjs エコシステム

Nitro のリクエストハンドリング基盤は H3 という軽量 HTTP フレームワーク(Express の「10分の1のサイズ」を標榜)である。H3 はランタイム非依存で、Node の IncomingMessage にも Web 標準の Request にも対応できるように設計されている。これが、Nitro の多様なプリセット対応を可能にしている鍵である。

Nitro と H3 の周辺には unjs エコシステムが広がっている。代表的なパッケージを挙げる。

パッケージ役割
h3HTTP ハンドラ(defineEventHandler, readBody など)
ofetchユニバーサル fetch ラッパー($fetch の実体)
unstorageKV ストレージ抽象(メモリ、Redis、Cloudflare KV など)
unimportAuto-imports の実体
c12設定ファイルローダー(nuxt.config.ts を読む)
consolaロガー
patheクロスプラットフォームなパス操作
listhen開発サーバー(HTTPS、ネットワーク公開などを簡単に)
nypmパッケージマネージャ抽象

これらは Nuxt 以外のプロジェクト(例えば Vite 拡張、CLI ツール)でも使われており、「JavaScript エコシステム横断の共通基盤」の形成を目指している。

2.5 ユニバーサルレンダリングのライフサイクル

SSR モードでのリクエストライフサイクルは次のようになる。

  1. リクエストが Nitro に到達
  2. server/middleware/ が順に実行
  3. ルートマッチング(Nitro ハンドラか Vue ページか)
  4. Vue ページの場合、app:render 前フック → Vue SSR (renderToString) 実行
  5. useAsyncData / useFetch がサーバー側で実行され、結果が payload として HTML に埋め込み
  6. ブラウザがレスポンスを受信し、ハイドレーション(payload を使って再計算を省略)
  7. クライアント側でナビゲーションが起こると、以降は SPA モードで遷移

この「サーバーで取得したデータをクライアントに持ち越す」仕組み(nuxtApp.payload)が、Nuxt の SSR が軽量に成立する理由である。クライアントではこの payload を読み取り、useAsyncData の再実行を省略する。

2.6 コマンドとプロジェクト起動

プロジェクト生成と基本コマンドは次の通り。

# 生成 (Nuxt 4 以降)
npx nuxi@latest init my-app
cd my-app
pnpm install

# 開発
pnpm dev          # http://localhost:3000

# 本番ビルド
pnpm build        # .output/ に生成

# 静的サイト生成
pnpm generate     # .output/public/ に HTML

# プレビュー
pnpm preview

.output ディレクトリは自己完結したサーバーバンドルで、node .output/server/index.mjs で起動できる。Docker イメージ化も容易である。

以降の章では、この構成要素を一つずつ掘り下げる。

3. ディレクトリ構造と規約

Nuxt は「規約による設定 (Convention over Configuration)」を徹底している。特定のディレクトリ名・ファイル名が特別な意味を持ち、これを外れた書き方は(可能ではあるが)推奨されない。Nuxt 4 以降は app/ ディレクトリを用いた新レイアウトが既定である。

3.1 トップレベル構造(Nuxt 4)

my-app/
├── app/                    # アプリケーションソース(Nuxt 4 の既定ルート)
│   ├── app.vue             # ルートコンポーネント
│   ├── error.vue           # グローバルエラーページ
│   ├── pages/              # ファイルベースのルート
│   ├── layouts/            # レイアウト
│   ├── components/         # 自動登録されるコンポーネント
│   ├── composables/        # 自動インポートされるコンポーザブル
│   ├── utils/              # 自動インポートされるユーティリティ
│   ├── middleware/         # ルートミドルウェア
│   ├── plugins/            # Nuxt プラグイン
│   └── assets/             # ビルドパイプラインを通る静的資産
├── server/                 # Nitro サーバーコード
│   ├── api/                # /api/* の自動ルート
│   ├── routes/             # 任意パスのサーバールート
│   ├── middleware/         # サーバーミドルウェア
│   ├── plugins/            # Nitro プラグイン
│   └── utils/              # サーバー側ユーティリティ
├── public/                 # そのまま配信される静的ファイル
├── shared/                 # app/ と server/ の両方から import 可能な共通コード
├── content/                # @nuxt/content を使う場合の記事ディレクトリ
├── nuxt.config.ts
├── app.config.ts           # ランタイム可変設定
├── package.json
├── tsconfig.json           # Nuxt が生成するものを継承
└── .nuxt/                  # 自動生成物(コミット対象外)

Nuxt 3 では app/ を使わず、pages/, components/ などをトップレベルに置くのが既定だった。Nuxt 4 では srcDir: 'app' が既定となり、アプリケーションコードとプロジェクトルート(設定ファイル、CI 設定、node_modules など)が明確に分離された。この分離は、モノレポや大型プロジェクトで非常に効果的である。

3.2 app.vue — ルートコンポーネント

app.vue は全ての描画の起点である。最小構成は次のとおり。

<!-- app/app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
  • NuxtLayoutapp/layouts/ のレイアウトをラップする。
  • NuxtPageapp/pages/ に基づくルーターアウトレット。
  • app/pages/ を作らなければ SPA 的に自作のテンプレートだけで完結させることも可能。

3.3 pages/ — ファイルベースルーティング

app/pages/ 以下のファイル構造がそのまま URL にマップされる。

app/pages/
├── index.vue                # /
├── about.vue                # /about
├── blog/
│   ├── index.vue            # /blog
│   └── [slug].vue           # /blog/:slug
├── users/
│   └── [id]/
│       ├── index.vue        # /users/:id
│       └── edit.vue         # /users/:id/edit
├── [...catchall].vue        # 404 相当のキャッチオール
└── (admin)/                 # グループ(URL には含まれない)
    └── dashboard.vue        # /dashboard
  • [param].vue で動的セグメント、[...all].vue でキャッチオール。
  • (groupName)/ルートグループ で、URL には現れないがレイアウトや権限の共通化に使える。
  • ネストしたルートは親ディレクトリに同名 .vue を置き、中で <NuxtPage /> を配置する。

動的ルートのパラメータは useRoute() か、definePageMeta で型付けできる。

<!-- app/pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute('blog-slug')  // ルート名から型が導出される
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
</script>

3.4 components/ — 自動登録

app/components/ に置かれた .vue ファイルは自動的にグローバル登録され、import 不要で使える。

app/components/
├── AppHeader.vue           # <AppHeader />
├── AppFooter.vue
├── Button/
│   ├── Primary.vue         # <ButtonPrimary />
│   └── Secondary.vue       # <ButtonSecondary />
└── content/
    └── Callout.vue         # <ContentCallout />

ネストディレクトリに置いたコンポーネントは PascalCase で連結 された名前になる(Button/Primary.vueButtonPrimary)。この命名を無効にするには components オプションで pathPrefix: false を指定する。

遅延コンポーネント

コンポーネント名の頭に Lazy を付けると、クライアント側で動的 import される。

<template>
  <LazyHeavyChart v-if="showChart" :data="data" />
</template>

クライアント/サーバー限定コンポーネント

  • *.client.vue — クライアントのみでロード
  • *.server.vue — サーバーのみ(Server Component 的に使う)
  • *.island.vue — Server Islands(インタラクティブ性のない部分をサーバーでだけ描画)

3.5 composables/utils/

app/composables/app/utils/ 配下の関数・変数は、デフォルトエクスポートと名前付きエクスポートの両方が Auto-imports の対象となる。

// app/composables/useCart.ts
export const useCart = () => {
  const items = useState<CartItem[]>('cart', () => [])
  const total = computed(() => items.value.reduce((s, i) => s + i.price * i.qty, 0))
  const add = (item: CartItem) => items.value.push(item)
  return { items, total, add }
}

上のコンポーザブルは、pages/*.vue からは const cart = useCart() とだけ書けば利用できる。useState の第一引数が「ユニークなキー」である点が重要で、これによって SSR から CSR への状態受け渡しが一意に決まる。

utils/ は副作用のない純粋関数や定数を置くのに適している。

// app/utils/format.ts
export const yen = (n: number) => new Intl.NumberFormat('ja-JP', {
  style: 'currency', currency: 'JPY'
}).format(n)

3.6 layouts/ — レイアウト

app/layouts/default.vue がデフォルトレイアウトとして自動適用される。他のレイアウトはページで definePageMeta({ layout: 'admin' }) のように指定する。

<!-- app/layouts/default.vue -->
<template>
  <div>
    <AppHeader />
    <main class="container">
      <slot />         <!-- ページがここに入る -->
    </main>
    <AppFooter />
  </div>
</template>

動的にレイアウトを切り替えたい場合は setPageLayout('admin') を使う。

3.7 middleware/ — ルートミドルウェア

ルート遷移時に実行されるミドルウェアを app/middleware/ に置く。

  • app/middleware/auth.tsdefinePageMeta({ middleware: 'auth' }) で個別適用
  • app/middleware/auth.global.ts — 全ルートに自動適用
// app/middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const user = useCurrentUser()
  if (!user.value && to.path.startsWith('/dashboard')) {
    return navigateTo('/login')
  }
})

ミドルウェアは サーバー・クライアント両方 で実行される。サーバー側では初回リクエストで、クライアント側では <NuxtLink> による遷移時に動作する。

3.8 plugins/ — プラグイン

app/plugins/ 配下の .ts / .js は Nuxt 起動時に一度だけ実行される。外部 SDK の初期化、Vue プラグインの登録、provide/inject の設定などに使う。

// app/plugins/posthog.client.ts
export default defineNuxtPlugin(() => {
  if (import.meta.client) {
    const { public: { posthogKey } } = useRuntimeConfig()
    posthog.init(posthogKey, { api_host: 'https://app.posthog.com' })
    return { provide: { posthog } }
  }
})

ファイル名末尾に .client / .server を付けるとクライアント限定・サーバー限定になる。

3.9 server/ — Nitro の領域

server/ は Nuxt アプリとは別の、Nitro のソース領域である。ここに書いたコードはサーバー側でのみ実行され、クライアントには一切バンドルされない。

  • server/api/hello.ts/api/hello
  • server/api/posts/[id].ts/api/posts/:id
  • server/routes/sitemap.xml.ts/sitemap.xml/api 以外の任意パス)
  • server/middleware/log.ts — 全リクエストに適用
  • server/plugins/db.ts — Nitro 起動時に実行(DB 接続など)
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const post = await usePostsRepo().findById(id!)
  if (!post) throw createError({ statusCode: 404, statusMessage: 'Not Found' })
  return post
})

ファイル名末尾の .get.ts / .post.ts / .delete.ts などは HTTP メソッドの宣言として機能する。

3.10 public/assets/

  • public/ — そのまま配信される。/robots.txt, /favicon.ico, 事前生成済み画像など。
  • assets/ — ビルドパイプライン(Vite)を通る。CSS, 画像インポート, フォントファイル。

例えば、コンポーネントの中から ~/assets/logo.png と書けば Vite が最適化・ハッシュ化してくれる。一方 public/logo.png は URL /logo.png でそのまま配信される。

3.11 shared/ ディレクトリ

Nuxt 4 で標準化された shared/ は、app/(クライアント+SSR)と server/(Nitro のみ)の両方から利用できる型・定数・ユーティリティの置き場である。TypeScript のパスエイリアス #shared でインポートする。

// shared/types/post.ts
export interface Post {
  id: string
  title: string
  body: string
  createdAt: string
}
// server/api/posts/index.get.ts
import type { Post } from '#shared/types/post'
export default defineEventHandler(async (): Promise<Post[]> => { /* ... */ })
<!-- app/pages/index.vue -->
<script setup lang="ts">
import type { Post } from '#shared/types/post'
const { data } = await useFetch<Post[]>('/api/posts')
</script>

この分離により、「サーバー専用の依存(DB クライアントなど)」がクライアントバンドルに紛れ込むのを防ぎつつ、ドメイン型は共有できる。

4. nuxt.config.ts — 設定の総覧

nuxt.config.ts はプロジェクトの中枢となる設定ファイルである。defineNuxtConfig は型補完を提供するヘルパで、返り値はオブジェクトそのもの。内部では c12 が読み込みを担当しており、nuxt.config.local.ts, .env, 環境別オーバーライド($development, $production)などが透過的にマージされる。

4.1 最小構成と環境別分岐

// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-04-01',
  devtools: { enabled: true },

  // 環境別分岐
  $development: {
    app: { head: { title: '[DEV] My Nuxt App' } }
  },
  $production: {
    app: { head: { title: 'My Nuxt App' } }
  }
})

compatibilityDate は Nitro のビヘイビアを「その日時点の挙動」に固定するためのキーで、プロジェクト生成時に自動で書き込まれる。これは future-proof のための仕組みで、Nuxt/Nitro 側に非互換変更が入っても既存プロジェクトが静かに壊れないようにしている。

4.2 よく使うトップレベルオプション

export default defineNuxtConfig({
  // ビルドとランタイム
  ssr: true,                      // false で SPA モード
  srcDir: 'app',                  // Nuxt 4 既定
  serverDir: 'server',
  builder: 'vite',

  // モジュール
  modules: [
    '@nuxt/content',
    '@nuxt/image',
    '@nuxt/ui',
    '@pinia/nuxt',
    '@vueuse/nuxt',
    ['@nuxtjs/i18n', { locales: ['ja', 'en'], defaultLocale: 'ja' }]
  ],

  // グローバル CSS
  css: ['~/assets/css/main.css'],

  // アプリメタ
  app: {
    head: {
      charset: 'utf-8',
      viewport: 'width=device-width, initial-scale=1',
      title: 'My App',
      meta: [
        { name: 'description', content: 'An example Nuxt app' }
      ],
      link: [
        { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }
      ]
    },
    pageTransition: { name: 'page', mode: 'out-in' },
    layoutTransition: { name: 'layout', mode: 'out-in' }
  },

  // ルーティング
  router: {
    options: { scrollBehaviorType: 'smooth' }
  },

  // コンポーネント自動登録の振る舞い
  components: [
    { path: '~/components', pathPrefix: false, global: true }
  ],

  // 自動インポート
  imports: {
    dirs: ['composables/**', 'stores', 'utils/**']
  },

  // ランタイム設定(後述)
  runtimeConfig: {
    apiSecret: '',
    public: { apiBase: '/api' }
  },

  // Route Rules(後述)
  routeRules: {
    '/':            { prerender: true },
    '/blog/**':     { isr: 60 },
    '/admin/**':    { ssr: false },
    '/api/_proxy/**': { proxy: 'https://upstream.example.com/**' }
  },

  // Nitro の細かい設定
  nitro: {
    preset: 'node-server',
    storage: {
      cache: { driver: 'redis', url: process.env.REDIS_URL }
    },
    compressPublicAssets: true,
    experimental: { wasm: true }
  },

  // 実験的機能
  experimental: {
    payloadExtraction: true,
    viewTransition: true,
    typedPages: true,
    asyncContext: true
  },

  // TypeScript
  typescript: {
    strict: true,
    typeCheck: 'build'   // vue-tsc を使ってビルド時に型チェック
  },

  // Vite 直接委譲
  vite: {
    css: { preprocessorOptions: { scss: { additionalData: '@use "~/assets/scss/_vars.scss" as *;' } } }
  },

  // PostCSS
  postcss: {
    plugins: { tailwindcss: {}, autoprefixer: {} }
  }
})

設定はかなり広範だが、すべて型補完が効くので IDE の支援を積極的に活用できる。

4.3 runtimeConfigapp.config.ts の使い分け

Nuxt には「ランタイムに読み込まれる値」を扱う仕組みが 2 つある。混同しやすいため、はっきり区別しておく。

項目runtimeConfigapp.config.ts
目的秘匿値・環境ごとに変わる値UI のテーマ・機能フラグなど
クライアント露出public 配下のみ全て露出
環境変数上書きNUXT_API_SECRET のように可能不可
変更の伝搬サーバー再起動が必要HMR で即時反映

runtimeConfig

// nuxt.config.ts
runtimeConfig: {
  apiSecret: '',                       // サーバー専用
  public: {
    apiBase: '/api',                   // クライアントにも露出
    sentryDsn: ''
  }
}
// 利用側
const config = useRuntimeConfig()
config.apiSecret          // サーバーでのみアクセス可(SSR でも可)
config.public.apiBase     // サーバー/クライアント両方

環境変数で上書きする場合、キーは NUXT_ プレフィックスを付けてスネークケース/キャメルケースを環境ごとに変換する。

# .env
NUXT_API_SECRET=super-secret
NUXT_PUBLIC_API_BASE=https://api.example.com

app.config.ts

// app.config.ts
export default defineAppConfig({
  theme: {
    primary: '#3b82f6',
    dark: false
  },
  features: {
    experimentalSearch: true
  }
})
<script setup lang="ts">
const appConfig = useAppConfig()
</script>

ビルド時に決まる値(ロゴ URL、カラー、フィーチャーフラグ)はこちらが適している。型は自動で拡張され、useAppConfig() のキーが補完される。

4.4 モジュール定義のインライン

modules 配列には、名前だけでなくインラインのモジュール関数も書ける。

modules: [
  // 既存モジュール
  '@pinia/nuxt',

  // インライン定義
  (_options, nuxt) => {
    nuxt.hook('pages:extend', (pages) => {
      pages.push({
        name: 'custom',
        path: '/custom',
        file: '~/custom-pages/custom.vue'
      })
    })
  }
]

小規模なカスタマイズなら専用モジュールを切らずに済むため、プロジェクト特有の規約を表現しやすい。

4.5 nuxt.config.ts のレイヤリング(Nuxt Layers)

extends を使うと、別プロジェクトや git リポジトリを「レイヤー」として継承できる。これは Nuxt 独自の強力な機能である。

export default defineNuxtConfig({
  extends: [
    '../base-layer',                              // モノレポ内の別パッケージ
    'github:my-org/nuxt-design-system#v2.0.0',    // GitHub から
    ['github:my-org/auth-layer', { install: true }]
  ]
})

レイヤーは components/, composables/, pages/, layouts/, server/, nuxt.config.ts をすべて重ね合わせる。大型組織で「共通 UI ライブラリ」「認証レイヤー」「ブログレイヤー」などを部品化したいときに有効である。

4.6 フック (hooks)

Nuxt の内部ライフサイクルにフックできる。典型例を挙げる。

export default defineNuxtConfig({
  hooks: {
    'pages:extend'(pages) {
      pages.push({ name: 'robots', path: '/robots.txt', file: '~/extra/robots.vue' })
    },
    'nitro:config'(nitroConfig) {
      nitroConfig.publicAssets ||= []
      nitroConfig.publicAssets.push({
        baseURL: '/docs',
        dir: '../docs/.vitepress/dist',
        maxAge: 60 * 60 * 24 * 365
      })
    },
    'build:manifest'(manifest) {
      // ビルド成果物のマニフェストを改変
    }
  }
})

フックはプラグイン・モジュール・設定のどこからでも登録でき、モジュール同士の協調の土台になっている。

4.7 環境変数と .env

Nuxt は起動時に .env を自動ロードする。ただし、dotenv で読むのはビルド前のみで、ランタイム(Docker コンテナで起動された後など)の環境変数は OS から読まれる。これは混乱しやすい点である。

  • ビルド時: .env がロードされ、runtimeConfig の初期値になる。
  • ランタイム時: NUXT_* プレフィックスの環境変数が runtimeConfig を上書きする。

したがって、コンテナ化してデプロイする場合は .env をイメージに焼き込むのではなく、Kubernetes Secret / AWS Secrets Manager / プラットフォームの環境変数機能を使って NUXT_API_SECRET=... を渡すのが正しい。

5. レンダリングモードとルートごとの制御

Nuxt の最大の強みの一つが、ルートごとに描画方式を切り替えられる Hybrid Rendering である。単一のアプリで、トップページだけ事前生成、ブログは ISR、管理画面は SPA、API プロキシはストリーミング、といった構成が宣言的に書ける。

5.1 基本の 4 モード

  • Universal (SSR) — 既定。サーバーで HTML を生成し、クライアントでハイドレーション。
  • Client-Only (SPA) — サーバーでは空の HTML、クライアントで全て描画。
  • Static (SSG / Prerender) — ビルド時に HTML を事前生成。CDN に置くだけで運用可。
  • ISR (Incremental Static Regeneration) — 一度生成した HTML を一定時間キャッシュし、裏で再生成。

グローバル切り替えは ssr: false で SPA モード、nuxt generate で SSG となる。ただし実務では、ルートルールでページごとに指定するのが推奨である。

5.2 routeRules によるルート単位の制御

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 事前生成
    '/':          { prerender: true },
    '/about':     { prerender: true },

    // ISR: 60 秒キャッシュ、期限切れ後は再生成
    '/blog/**':   { isr: 60 },

    // SWR: ブラウザに Cache-Control ヘッダを付けつつ背後で再検証
    '/products/**': { swr: 3600 },

    // SPA 化(SSR を切る)
    '/admin/**':  { ssr: false },

    // 他オリジンへプロキシ
    '/api/legacy/**': { proxy: 'https://legacy.example.com/**' },

    // セキュリティヘッダ
    '/**': {
      headers: {
        'X-Frame-Options': 'DENY',
        'Referrer-Policy': 'strict-origin-when-cross-origin'
      }
    },

    // リダイレクト
    '/old-page': { redirect: '/new-page' },

    // CORS を開ける
    '/api/public/**': { cors: true }
  }
})
  • prerender: truenuxi build 実行時に HTML を生成し、静的ファイルとして .output/public に配置。
  • isr: 60 — 60 秒間はキャッシュ HTML を返し、次のリクエストで裏ビルド。
  • isr: true — 期限なし。初回のみ生成。
  • swrstale-while-revalidate。CDN でも有効な Cache-Control を送出。
  • ssr: false — そのルートは SPA として処理。初期 HTML は空。

これらは Nitro のプリセットが対応するプラットフォームで自動的に最適な形へ変換される。たとえば Vercel なら ISR は Vercel 固有の ISR 機能に、Cloudflare なら Cache API へのラッパーに変換される。

5.3 definePageMeta によるページレベル制御

ページ側から制御する方法もある。

<!-- app/pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  layout: 'admin',
  middleware: ['auth'],
  ssr: false                 // このページだけ SPA
})
</script>

definePageMeta はビルド時にコンパイラマクロとして処理され、pages/ のルート情報に反映される。したがって実行時オーバーヘッドはない。

5.4 動的ルートの事前生成

prerender は、リンクを辿ってクロールすることで動的ルートも発見してくれる。しかし、どのページからもリンクされていない動的ルートは明示列挙が必要だ。

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: ['/sitemap.xml'],
      ignore: ['/admin']
    }
  },
  hooks: {
    async 'nitro:config'(nitroConfig) {
      const slugs = await fetch('https://cms.example.com/slugs').then(r => r.json())
      nitroConfig.prerender!.routes!.push(...slugs.map((s: string) => `/blog/${s}`))
    }
  }
})

5.5 Payload Extraction

静的ルートに対しては、ハイドレーション用の payload を別ファイル(_payload.js)として切り出す最適化が行われる。これにより、HTML のサイズを小さく保ちつつ、クライアントサイドナビゲーション時にも payload を再利用できる。experimental.payloadExtraction: true で明示できるが、nuxt generate では既定で有効である。

5.6 Server Components と Server Islands

Nuxt は React Server Components 風の「サーバー限定コンポーネント」をサポートする。

  • Server Components (*.server.vue) — サーバーでだけレンダリングされ、クライアントには HTML しか送られない。
  • Server Islands (*.island.vue) — 複数の「島」をサーバーでバラバラに描画し、クライアントは各島を遅延取得できる。

典型的なユースケースは、重い SSR 処理(例: Markdown レンダリング、SVG チャート生成)をクライアントバンドルから外すこと。

<!-- app/components/PostBody.server.vue -->
<script setup lang="ts">
import { marked } from 'marked'   // サーバーだけにバンドル
const props = defineProps<{ markdown: string }>()
const html = computed(() => marked(props.markdown))
</script>
<template>
  <article v-html="html" />
</template>

クライアントにはインタラクションのない純粋な DOM だけが届くため、marked を含む数百 KB が削減できる。

5.7 View Transitions

experimental.viewTransition: true を有効にすると、Nuxt は View Transitions API を使ったルート遷移を行う。画像を共有したカードが、詳細ページでヒーローに拡大するといった演出が数行の CSS で可能になる。

/* app/assets/css/transitions.css */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.25s;
}
<NuxtLink :to="`/posts/${post.id}`" :view-transition-name="`post-${post.id}`">
  <article :style="{ viewTransitionName: `post-${post.id}` }">...</article>
</NuxtLink>

6. ルーティング詳論

6.1 ファイルベースルートの型生成

Nuxt 3 後期から安定化した experimental.typedPages: true により、ルートが型安全になる。

export default defineNuxtConfig({
  experimental: { typedPages: true }
})

これを有効にすると、以下のようにルート名・パラメータがすべて補完・検証される。

<script setup lang="ts">
const route = useRoute('users-id')          // 'users-id' は /users/[id] の自動名
route.params.id                             // 型: string

await navigateTo({ name: 'users-id', params: { id: '123' } })
// typo するとコンパイルエラー
</script>

<template>
  <NuxtLink :to="{ name: 'users-id', params: { id: user.id } }">詳細</NuxtLink>
</template>

これは unplugin-vue-router による生成物を、Nuxt が取り込む形で実現されている。

6.2 ネストルート

親に /parent.vue、子に /parent/child.vue を置くと、親の中で <NuxtPage /> を書くことで子が描画される。

<!-- app/pages/dashboard.vue -->
<template>
  <div class="dashboard">
    <aside>
      <NuxtLink to="/dashboard">概要</NuxtLink>
      <NuxtLink to="/dashboard/analytics">分析</NuxtLink>
    </aside>
    <section>
      <NuxtPage />
    </section>
  </div>
</template>

6.3 プログラマティックナビゲーション

  • navigateTo(to) — 内部でプラットフォームを判定して router.push or sendRedirect
  • abortNavigation(reason?) — ミドルウェア内で遷移を中断。
  • useRouter() — Vue Router のインスタンス。
  • useRoute() — 現在のルート。
// middleware/role.ts
export default defineNuxtRouteMiddleware((to) => {
  const { role } = useUserSession()
  if (to.meta.requiresAdmin && role.value !== 'admin') {
    return abortNavigation({ statusCode: 403, message: '権限がありません' })
  }
})

6.4 並列ルートと名前付きビュー

Vue Router の components オプションを使えば、一つの URL に複数のコンポーネントを同時に載せられる。Nuxt では definePageMeta で親の <NuxtPage name="sidebar" /> と組み合わせる形で応用できる。大型管理画面の「本体 + サイドパネル」のような UI に有効である。

6.5 ナビゲーションのライフサイクル

<NuxtLink> をクリックしたときの順序は以下である。

  1. グローバルミドルウェア
  2. 対象ルートのミドルウェア
  3. useAsyncData / useFetch 再実行(キャッシュキーが異なる場合)
  4. コンポーネントの setup()
  5. DOM 更新とトランジション

この順序を理解しないと「ミドルウェアで取得したデータがコンポーネントで使えない」という誤解が生まれやすい。ミドルウェアで先行取得したい場合は、useAsyncDatakey を共有するか、nuxtApp.payload に書き込む。

7. データ取得 — useFetch / useAsyncData / $fetch

Nuxt のデータ取得 API は 3 つあり、それぞれ役割が異なる。

APIサーバーで実行キャッシュ共有用途
$fetch実行箇所依存しない命令的な取得(イベントハンドラ等)
useAsyncDataする(既定)する任意の非同期処理の結果を SSR payload に載せる
useFetchする(既定)するuseAsyncData + $fetch のシンタックスシュガー

7.1 $fetch

$fetchofetch ベースのユニバーサル HTTP クライアントで、サーバーでもクライアントでも動く。Node/Workers 上でも fetch が存在する環境を要求しない。SSR 時に自分自身の API を叩く場合は、内部 fetch パスとして処理され HTTP ラウンドトリップが発生しない(Nitro が直接ハンドラを呼ぶ)。

const onSubmit = async () => {
  try {
    await $fetch('/api/posts', { method: 'POST', body: form.value })
    navigateTo('/posts')
  } catch (err) {
    // ofetch は 4xx/5xx を自動で throw する
  }
}

7.2 useAsyncData

任意の非同期処理結果を SSR 時に取得し、クライアントに payload として引き渡す。

const { data, pending, error, refresh } = await useAsyncData(
  'user-profile',                                    // キャッシュキー
  () => getUserProfile(route.params.id as string),
  {
    lazy: false,
    server: true,
    default: () => ({ name: '', posts: [] }),
    transform: (raw) => ({ ...raw, fullName: `${raw.first} ${raw.last}` }),
    watch: [() => route.params.id],
    getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
  }
)
  • key は手動指定でもよいし、呼び出し箇所のファイル・行から自動生成される。
  • watch に指定したリアクティブ参照が変わると自動で再取得。
  • getCachedData で、クライアント側ナビゲーション時に payload を使って API 再呼び出しを省略できる(重要)。
  • lazy: true にすると、await しても即座に返り、フェッチは裏で進む(pending を UI に反映)。

7.3 useFetch

useAsyncData$fetch のラッパーを組み合わせた糖衣。URL を与えるだけで、SSR 時に取得・payload 化・クライアントへの引き渡しまで自動化される。

const { data } = await useFetch('/api/posts', {
  query: { limit: 10 },
  headers: { 'X-Requested-With': 'Nuxt' },
  onRequest({ options }) { /* 認証ヘッダなど */ },
  onResponseError({ response }) { /* 共通エラー処理 */ },
  timeout: 5000
})

クエリパラメータや URL が動的な場合は関数形で渡すこと

// ❌ 悪例:route.params.id が変わっても再取得されない
const { data } = await useFetch(`/api/posts/${route.params.id}`)

// ✅ 良い例
const { data } = await useFetch(() => `/api/posts/${route.params.id}`, {
  watch: [() => route.params.id]
})

7.4 エラーハンドリング

サーバー側ハンドラで createError を投げると、useFetcherror に Nuxt 標準エラー型が入る。

// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const post = await findPost(id!)
  if (!post) {
    throw createError({ statusCode: 404, statusMessage: 'Post not found', fatal: false })
  }
  return post
})

クライアント側ではこれを受けて、error.value?.statusCode で分岐できる。全体的な致命エラーは showError() / clearError() でエラーページを出せる。

7.5 キャッシュと再検証

Nuxt 3.10 以降、useFetchgetCachedData を既定で備え、同じキーへのナビゲーションでは payload を再利用する。強制的に再取得したい場合は refresh() または refreshNuxtData(key) を呼ぶ。

const { data, refresh } = await useFetch('/api/me')
await $fetch('/api/me', { method: 'PATCH', body: { name } })
await refresh()
// あるいは他コンポーネントの useFetch('/api/me') も巻き込むなら
await refreshNuxtData('/api/me')

8. 状態管理

8.1 useState — 内蔵の SSR セーフな状態

useState(key, factory) は SSR/CSR 間で状態を自動同期する、Nuxt 最小の状態ストアである。

// composables/useCounter.ts
export const useCounter = () => useState('counter', () => 0)

useState の特徴:

  • キー(第一引数)単位でシングルトン。
  • サーバーで初期化された値は payload 経由でクライアントに届く。
  • ref を返すので、Composition API の一部として自然に使える。
  • refreactive を直接エクスポートしてはいけない(モジュールスコープだとリクエスト間で共有されてしまう)。useState を使うことでリクエストごとのスコープが保たれる。

8.2 Pinia — 実戦標準の状態管理

より構造化が必要なら @pinia/nuxt を使う。

pnpm add @pinia/nuxt pinia
// nuxt.config.ts
modules: ['@pinia/nuxt']
// app/stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const total = computed(() => items.value.reduce((s, i) => s + i.price * i.qty, 0))
  const add = (item: CartItem) => items.value.push(item)
  const clear = () => items.value = []
  return { items, total, add, clear }
})
<script setup lang="ts">
const cart = useCartStore()
// cart.items, cart.total, cart.add(...)
</script>

Pinia も useState と同様に SSR payload に載るので、サーバーで初期化したストアをそのままクライアントに引き継げる。

8.3 useCookie — Cookie ベースの状態

リクエスト/レスポンス両方を扱う useCookie は、認証トークンやテーマ設定に便利。

const theme = useCookie<'light' | 'dark'>('theme', { default: () => 'light', maxAge: 60 * 60 * 24 * 365 })
theme.value = 'dark'   // 自動で Set-Cookie

8.4 nuxtApp.payload — 低レベル API

稀に、コンポーザブルや useAsyncData を経由せずに、サーバーからクライアントへ任意のデータを渡したい場合がある。

// サーバー側プラグイン
export default defineNuxtPlugin((nuxtApp) => {
  if (import.meta.server) {
    nuxtApp.payload.featureFlags = getFeatureFlagsForRequest()
  }
})
// 利用側
const { $featureFlags } = useNuxtApp()

ただし、payload は JSON シリアライズ可能な値に限られる点に注意(Map/Set/Date は payload.serialize による処理が必要)。

9. Nitro サーバーエンジンと server/ ディレクトリ

Nuxt アプリが「フルスタック」と呼べる理由の大部分は、Nitro によって提供される サーバーサイド機能 にある。単なる BFF(Backend For Frontend)としてだけでなく、完全なバックエンド API、WebSocket、キャッシング、バックグラウンドタスク(スケジュール実行)まで表現できる。

9.1 API ルート

server/api/ 配下の .ts / .js ファイルが /api/<path> に自動マップされる。

server/api/
├── health.ts              # GET /api/health
├── posts/
│   ├── index.get.ts       # GET /api/posts
│   ├── index.post.ts      # POST /api/posts
│   └── [id].get.ts        # GET /api/posts/:id
└── auth/
    ├── login.post.ts
    └── logout.post.ts

HTTP メソッドはファイル名末尾の .get / .post / .put / .patch / .delete で制限できる。複数メソッドを 1 ファイルにまとめるには getMethod(event) で分岐する。

// server/api/posts/index.post.ts
import { z } from 'zod'

const CreatePost = z.object({
  title: z.string().min(1).max(200),
  body: z.string().min(1)
})

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, CreatePost.parse)
  const user = await requireAuth(event)

  const post = await usePostsRepo().create({ ...body, authorId: user.id })
  setResponseStatus(event, 201)
  return post
})

readValidatedBody は H3 が提供する機能で、zod や valibot と組み合わせやすい。defineEventHandler のジェネリクスで戻り値の型を明示すれば、クライアント側の useFetch が自動で型推論する(後述)。

9.2 ルート

/api/ 以外の任意パスを扱いたい場合は server/routes/ を使う。

// server/routes/sitemap.xml.ts
export default defineEventHandler(async (event) => {
  const posts = await usePostsRepo().all()
  setHeader(event, 'content-type', 'application/xml')
  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts.map(p => `<url><loc>https://example.com/posts/${p.slug}</loc></url>`).join('\n')}
</urlset>`
})

9.3 サーバーミドルウェア

server/middleware/*.ts は全リクエストで実行される。認証、ログ、ヘッダ付与、レート制限などに使う。

// server/middleware/timing.ts
export default defineEventHandler(async (event) => {
  const start = Date.now()
  event.node.res.on('finish', () => {
    console.log(`${event.method} ${event.path} ${Date.now() - start}ms`)
  })
})

ミドルウェアは戻り値を返さない(返すとそこで応答が確定してしまう)。条件分岐で早期返却したい場合は sendRedirect / sendError などを使う。

9.4 Nitro プラグイン

server/plugins/*.ts は Nitro 起動時に一度だけ走る。DB 接続、キャッシュ初期化、定期タスク登録などに使う。

// server/plugins/db.ts
import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'

export default defineNitroPlugin((nitro) => {
  const config = useRuntimeConfig()
  const db = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: config.dbUrl }) }) })
  nitro.hooks.hook('close', async () => { await db.destroy() })
  // 全ハンドラから useDb() で取得できるようにする
  globalThis.__db = db
})

9.5 ストレージ API(unstorage

Nitro は unstorage を内蔵しており、一貫した KV インターフェースでさまざまなバックエンドを扱える。

// nuxt.config.ts
nitro: {
  storage: {
    cache: { driver: 'redis', url: process.env.REDIS_URL },
    sessions: { driver: 'cloudflareKVBinding', binding: 'SESSIONS' },
    fs: { driver: 'fs', base: './.data/fs' }
  }
}
// server/api/views/[slug].ts
export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug')!
  const cache = useStorage('cache')
  const key = `views:${slug}`
  let count = await cache.getItem<number>(key) ?? 0
  count++
  await cache.setItem(key, count, { ttl: 60 * 60 })
  return { slug, count }
})

開発中はメモリに、本番では Redis に、エッジでは Cloudflare KV に、というようにコードを変えずに切り替えられるのが unstorage の真価である。

9.6 キャッシュ関数 defineCachedEventHandler

重いハンドラを透過的にキャッシュしたいとき、Nitro はヘルパを提供する。

// server/api/expensive.ts
export default defineCachedEventHandler(async (event) => {
  return await heavyComputation()
}, {
  maxAge: 60,                      // 60 秒
  swr: true,                       // stale-while-revalidate
  base: 'cache',                   // storage mount 名
  getKey: (event) => event.path,
  shouldBypassCache: (event) => getQuery(event).nocache === '1'
})

defineCachedFunction で任意の非同期関数をキャッシュすることもできる。これは useAsyncData のサーバー版に近い。

9.7 スケジュール実行とタスク

Nitro 2 系では Scheduled Tasks が公式サポートされている。

// nuxt.config.ts
nitro: {
  experimental: { tasks: true },
  scheduledTasks: {
    '0 * * * *': ['cache:clear'],
    '0 0 * * *': ['reports:daily']
  }
}
// server/tasks/cache/clear.ts
export default defineTask({
  meta: { name: 'cache:clear', description: '期限切れキャッシュを削除' },
  async run({ payload }) {
    const cache = useStorage('cache')
    const keys = await cache.getKeys()
    for (const key of keys) { /* TTL チェック */ }
    return { result: 'ok' }
  }
})

これらはプラットフォーム次第では cron 連携(Cloudflare Cron Triggers、Vercel Cron)として展開される。

9.8 WebSocket

Nitro 2 以降は WebSocket をネイティブサポートする(crossws 経由)。

// server/routes/_ws.ts
export default defineWebSocketHandler({
  open(peer) { peer.send('welcome') },
  message(peer, message) {
    peer.publish('room', `${peer.id}: ${message.text()}`)
  },
  close(peer) { peer.unsubscribe('room') }
})
// nuxt.config.ts
nitro: { experimental: { websocket: true } }

9.9 内部 $fetch と型連携

サーバーハンドラが defineEventHandler(() => Post) と返り値の型を持つとき、クライアント側の useFetch('/api/posts/1') はその型を自動で推論する。これを可能にしているのが、Nuxt が生成する server API スキーマで、.nuxt/types/ に書き出される NitroFetchRequestInternalApi の型である。

// server/api/posts/[id].get.ts
export default defineEventHandler(async (event): Promise<Post> => {
  // ...
  return post
})
<script setup lang="ts">
const { data } = await useFetch('/api/posts/1')
//       ^? Ref<Post | null>
</script>

このエンドツーエンド型安全性は、tRPC を持ち込まずとも Nuxt だけで達成できる強力な機能である。

10. 自動インポートとコンポーザブル

10.1 Auto-imports の仕組み

Auto-imports は unimport が実装の中核で、次の要素を対象にしている。

  • Vue の API(ref, computed, watch, onMounted, ...)
  • Nuxt のコンポーザブル(useFetch, useState, useRoute, ...)
  • composables/ 配下の関数
  • utils/ 配下の関数
  • モジュールが提供するコンポーザブル(例: @vueuse/nuxtuseDark()

これらはコンパイル時にスキャンされ、使われている箇所だけに import 文が差し込まれる。つまり、Tree-shaking は壊れない。Runtime 的には素の ESM と変わらない。

10.2 Auto-imports の無効化・制限

「グローバルっぽく見えて混乱する」「プロジェクトのルールに合わない」と感じたら、選択的に無効化できる。

// nuxt.config.ts
imports: {
  autoImport: true,                    // 全体オフにするなら false
  dirs: ['composables/**', 'utils/**'],
  imports: [
    { from: 'vue-i18n', name: 'useI18n' }
  ]
},
components: {
  global: false,                       // コンポーネント自動登録もオフに
  dirs: [{ path: '~/components/ui', prefix: 'Ui' }]
}

10.3 コンポーザブルの設計パターン

Nuxt でのコンポーザブルは「Vue Composition API を返す関数」だが、いくつかの設計上の注意がある。

① 状態はモジュールスコープではなく useState

// ❌ リクエスト間で状態が共有される(SSR でバグ)
const count = ref(0)
export const useCounter = () => ({ count })

// ✅ リクエストスコープの状態
export const useCounter = () => useState('counter', () => 0)

② 副作用は onMountedimport.meta.client で保護

export const useWindowWidth = () => {
  const width = ref(0)
  if (import.meta.client) {
    onMounted(() => {
      width.value = window.innerWidth
      window.addEventListener('resize', () => width.value = window.innerWidth)
    })
  }
  return width
}

useFetch を内包するラッパーを作るときは key を制御

export const useUser = (id: MaybeRef<string>) =>
  useFetch(() => `/api/users/${unref(id)}`, {
    key: () => `user-${unref(id)}`,
    default: () => null
  })

10.4 便利な組み込みコンポーザブル

  • useRoute, useRouter, navigateTo
  • useAsyncData, useFetch, useLazyFetch, useLazyAsyncData
  • useState, useCookie
  • useHead, useSeoMeta, useServerSeoMeta
  • useRuntimeConfig, useAppConfig
  • useNuxtApp, useError, showError, clearError
  • useRequestHeaders, useRequestURL, useRequestEvent(サーバー限定情報)
  • useNuxtData(既存キャッシュを参照のみ)
  • refreshCookie, refreshNuxtData

特に useRequestHeaders(['cookie']) は SSR 時に上流のヘッダを API にパススルーするのに使う。Cookie ベース認証の典型パターンは次のようになる。

const { data: user } = await useFetch('/api/me', {
  headers: useRequestHeaders(['cookie'])
})

11. モジュールシステムとエコシステム

11.1 モジュールの役割

Nuxt モジュールは、設定・コンポーネント・コンポーザブル・サーバーハンドラをパッケージとして配布する仕組み。モジュールは次のようなことをまとめて行える。

  • nuxt.config.ts への追加設定
  • components/ / composables/ / plugins/ の追加
  • server/api ルートの追加
  • 型定義の追加
  • ビルド前のコード生成

11.2 主要な公式 / 準公式モジュール

モジュール提供機能
@nuxt/contentMarkdown/YAML ベースの CMS(MDC、コードハイライト、クエリ)
@nuxt/image画像最適化(IPX、外部プロバイダ)
@nuxt/uiTailwind ベースの UI コンポーネント集
@nuxt/fontsGoogle/Local フォントの自動最適化
@nuxt/iconアイコンセット統合(Iconify)
@nuxt/test-utilsテストユーティリティ
@nuxt/scriptsサードパーティスクリプトの最適ロード
@nuxt/eslintESLint 設定の自動生成
@pinia/nuxtPinia 統合
@vueuse/nuxtVueUse の Auto-imports
@nuxtjs/i18n国際化
@nuxtjs/tailwindcssTailwind 統合
@nuxtjs/seoサイトマップ、robots、OGP など
@sidebase/nuxt-auth認証(NextAuth.js の Nuxt 版)

11.3 簡単な設定例:@nuxt/image + @nuxt/content

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image', '@nuxt/content'],
  image: {
    provider: 'ipx',
    quality: 80,
    format: ['webp', 'avif'],
    densities: [1, 2],
    screens: { xs: 320, sm: 640, md: 768, lg: 1024, xl: 1280 }
  },
  content: {
    highlight: {
      theme: { default: 'github-light', dark: 'github-dark' },
      langs: ['ts', 'vue', 'bash', 'json']
    },
    experimental: { clientDB: true }
  }
})
<template>
  <NuxtImg
    src="/hero.jpg"
    alt="Hero"
    sizes="sm:100vw md:50vw lg:400px"
    loading="lazy"
    format="webp"
  />
</template>

11.4 自作モジュール

モジュールは defineNuxtModule で定義する。

// modules/analytics/index.ts
import { defineNuxtModule, addPlugin, addImports, createResolver } from '@nuxt/kit'

export interface ModuleOptions {
  id: string
  enabled?: boolean
}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: 'analytics',
    configKey: 'analytics',
    compatibility: { nuxt: '>=3.0.0' }
  },
  defaults: { enabled: true },
  setup(options, nuxt) {
    if (!options.enabled) return
    const { resolve } = createResolver(import.meta.url)

    // runtimeConfig に追加
    nuxt.options.runtimeConfig.public.analyticsId = options.id

    // プラグイン登録
    addPlugin(resolve('./runtime/plugin.client'))

    // コンポーザブルの追加
    addImports({
      name: 'useAnalytics',
      from: resolve('./runtime/composables/useAnalytics')
    })
  }
})

モジュールは @nuxt/kit のユーティリティを駆使して、通常 nuxt.config.ts でやることをプログラマティックに実施する。

12. プラグイン、ミドルウェア、レイアウト、エラー処理

12.1 プラグインの詳細

プラグインは次の制御キーで挙動を微調整できる。

// app/plugins/analytics.client.ts
export default defineNuxtPlugin({
  name: 'analytics',
  enforce: 'post',                     // 'pre' | 'default' | 'post'
  parallel: true,                      // 他プラグインと並列起動
  async setup(nuxtApp) {
    const { public: { gaId } } = useRuntimeConfig()
    const ga = await loadGoogleAnalytics(gaId)
    nuxtApp.vueApp.provide('ga', ga)

    nuxtApp.hook('page:finish', () => ga.pageview(useRoute().fullPath))
  },
  hooks: {
    'app:error'(err) { /* エラー通知 */ }
  }
})

プラグイン内で provide を返すと、useNuxtApp()$ プレフィックス付きで取り出せる。

export default defineNuxtPlugin(() => ({
  provide: { hello: (name: string) => `Hello, ${name}` }
}))

// 利用側
const { $hello } = useNuxtApp()
$hello('世界')

型は plugins.d.tsdeclare module '#app' として拡張しておくと補完が効く。

12.2 ルートミドルウェア

  • 名前付き: app/middleware/auth.tsdefinePageMeta({ middleware: 'auth' })
  • グローバル: app/middleware/auth.global.ts → 全ルート
  • インライン: ページ内に無名関数として書ける
<script setup lang="ts">
definePageMeta({
  middleware: [
    'auth',
    function (to, from) {
      if (to.query.preview && !from.query.preview) { /* ... */ }
    }
  ]
})
</script>

サーバー側で重いチェック(DB 参照など)をやりたい場合は、server/middleware/ 側で行うのが原則。ルートミドルウェアはクライアントでも走るため、機密情報や重い処理を入れない。

12.3 レイアウトの動的切替

<script setup lang="ts">
const auth = useUserSession()
watch(auth.loggedIn, (v) => setPageLayout(v ? 'app' : 'marketing'), { immediate: true })
</script>

ネストしたレイアウトも作れる。layouts/admin.vue の中に <NuxtLayout name="sidebar"> と書けば、ラップ構造になる。

12.4 エラーページ

  • app/error.vue は 404 や致命エラー時に全画面で表示される。
  • useError() で現在のエラーを取得。
  • clearError({ redirect: '/' }) でリカバリ。
<!-- app/error.vue -->
<script setup lang="ts">
const error = useError()
const handle = () => clearError({ redirect: '/' })
</script>

<template>
  <div class="grid place-items-center min-h-screen">
    <div class="text-center">
      <h1 class="text-4xl">{{ error?.statusCode }}</h1>
      <p>{{ error?.message }}</p>
      <button @click="handle">ホームへ</button>
    </div>
  </div>
</template>

12.5 アプリのメタ情報

useHead, useSeoMeta, useServerSeoMeta で SSR 対応のメタタグを設定する。

useSeoMeta({
  title: post.value.title,
  description: post.value.excerpt,
  ogImage: `https://example.com/og/${post.value.slug}.png`,
  twitterCard: 'summary_large_image'
})

useServerSeoMeta はクライアント側では何もしないため、静的に決まる値はこちらで。

13. TypeScript 連携

13.1 自動生成される型の世界

Nuxt は開発時、.nuxt/tsconfig.json とそれを継承する .nuxt/tsconfig.*.json を自動生成する。プロジェクトの tsconfig.json は通常、これらを extends するだけで良い。

// tsconfig.json
{
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "strict": true
  }
}

自動生成には、以下が含まれる。

  • ルート名の Union 型(typedPages 有効時)
  • server/api/* から導出される InternalApiuseFetch の型推論)
  • Auto-imports の global.d.ts
  • モジュールが拡張した NuxtApp, RuntimeConfig, AppConfig の型
  • #imports, #app, #shared などの仮想モジュール

13.2 runtimeConfig の型

runtimeConfig は初期値から型が推論されるが、環境変数で上書きされる想定の値については明示的な型が必要なことがある。

// nuxt.config.ts
runtimeConfig: {
  apiSecret: '' as string,        // 推論に任せる
  rateLimit: 100,                 // number として推論
  public: {
    apiBase: '/api',
    sentryDsn: ''
  }
}

declare module 'nuxt/schema' で型拡張も可能。

// index.d.ts
declare module 'nuxt/schema' {
  interface PublicRuntimeConfig { sentryDsn: string }
  interface RuntimeConfig { apiSecret: string }
}
export {}

13.3 イベントハンドラの型

サーバーハンドラで Zod を使う例は以前示した通りだが、戻り値の型をきっちり付けておくと、クライアントの useFetch が正確に推論される。

// server/api/posts/index.get.ts
import type { Post } from '#shared/types/post'

export default defineEventHandler(async (event): Promise<{ items: Post[]; total: number }> => {
  const q = getQuery(event)
  const limit = Math.min(Number(q.limit ?? 20), 100)
  return { items: await repo.list({ limit }), total: await repo.count() }
})

13.4 Vue コンポーネントの型推論

<script setup lang="ts"> ではジェネリクスを使えるので、汎用コンポーネントを型安全に書ける。

<script setup lang="ts" generic="T extends { id: string | number }">
const props = defineProps<{ items: T[] }>()
const emit = defineEmits<{ select: [item: T] }>()
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id" @click="emit('select', item)">
      <slot :item="item">{{ item.id }}</slot>
    </li>
  </ul>
</template>

13.5 vue-tsc によるビルド型チェック

typescript.typeCheck: 'build' を有効にすると、nuxt build 時に vue-tsc で型チェックが走る。CI での型エラー検出に有効。

14. パフォーマンス最適化

Nuxt でパフォーマンスを引き出すには、フレームワーク固有の最適化ポイントを押さえておく必要がある。

14.1 Route Rules による分類

前述の routeRules は最強のパフォーマンス手段である。変更頻度の低いページは Prerender、中程度は ISR、高頻度の動的ページのみ SSR という分類を徹底する。

routeRules: {
  '/':             { prerender: true },
  '/pricing':      { prerender: true },
  '/blog':         { isr: 600 },
  '/blog/**':      { isr: 60 },
  '/product/**':   { swr: 120 },
  '/dashboard/**': { ssr: true },
  '/admin/**':     { ssr: false }
}

14.2 コンポーネントコード分割

  • LazyXxx 接頭辞での動的読み込み。
  • defineAsyncComponent で Suspense 対応の非同期コンポーネント。
  • 重い依存を持つコンポーネントは *.client.vue に寄せてサーバーバンドルから除外。

14.3 Payload Extraction

nuxt generate では既定で _payload.json が別出力され、Hydration に必要なデータが HTML から分離される。これにより HTML がキャッシュ効率良くなる。

14.4 画像最適化

@nuxt/image は IPX(Node サーバー)、ipxStatic(静的プリジェネレート)、Cloudinary、Vercel、Cloudflare Images などのプロバイダをサポートする。<NuxtPicture><picture> 要素を生成し、AVIF/WebP フォールバックを自動で付ける。

14.5 フォント最適化

@nuxt/fonts を使うと、Google Fonts/Bunny Fonts/Fontshare などのソースから、ローカルにフォントファイルをダウンロードしてキャッシュし、@font-face を自動生成する。これによりサードパーティ由来の CLS(レイアウトシフト)と接続のオーバーヘッドが削減される。

modules: ['@nuxt/fonts'],
fonts: {
  families: [
    { name: 'Inter', weights: [400, 600], provider: 'google' },
    { name: 'Noto Sans JP', weights: [400, 700], provider: 'google', subsets: ['japanese'] }
  ]
}

14.6 Server Islands でバンドル削減

*.island.vue を活用すると、クライアントにはほぼ HTML しか届かない領域を作れる。コードハイライト、Markdown レンダリング、大きなアイコンセットなどに有効。

14.7 キャッシュヘッダと CDN

routeRulesswr, isr, headers は、実行プラットフォーム固有のキャッシュ機構へ適切に変換される。Cloudflare なら Cache-ControlVary が適切に付与され、Workers のキャッシュ API と連携する。

14.8 メトリクス計測

useScript@nuxt/scripts モジュールは、サードパーティスクリプト(GA4, GTM, Segment, Meta Pixel)を遅延・帯域制限しながら読み込める。これはモバイル 4G などの厳しい回線での LCP 改善に効果的。

14.9 重要な落とし穴

  • SSR 中の重い処理はレスポンス時間に直撃する。API 呼び出しの並列化や defineCachedFunction の活用を。
  • useState のキー重複で状態が混線する。キーの命名規則を決めること。
  • ref をモジュールスコープで公開するとリクエスト間リーク。必ず useState か factory 関数経由で。
  • process.client / process.server は Nuxt 3 まで、Nuxt 4 では import.meta.client / import.meta.server に統一。

15. デプロイとプロダクション運用

15.1 ビルド成果物

.output/
├── nitro.json
├── public/             # 静的資産(クライアントバンドル、prerender HTML)
└── server/
    ├── index.mjs       # エントリポイント
    ├── chunks/
    └── node_modules/   # サーバーサイドの依存

自己完結しているため、node .output/server/index.mjs だけで起動できる。

15.2 Node.js 向け

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm i --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output ./.output
ENV HOST=0.0.0.0 PORT=3000 NODE_ENV=production
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

15.3 Vercel

NITRO_PRESET=vercel か、Vercel が自動検知で選択。ISR / Prerender / Edge Functions に対応。

// nuxt.config.ts
routeRules: {
  '/':        { prerender: true },
  '/blog/**': { isr: 60 },
  '/edge/**': { isr: { expiration: 60 }, experimentalNoScripts: false }
}

Edge Runtime を指定するには nitro.preset: 'vercel-edge' を使うが、Node 固有 API は使えなくなるため依存をよく吟味すること。

15.4 Cloudflare Pages / Workers

NITRO_PRESET=cloudflare-pages を設定。wrangler.toml で KV や Durable Objects をバインドし、server/plugins/ から event.context.cloudflare 経由で触る。

# wrangler.toml
name = "my-nuxt-app"
compatibility_date = "2024-11-01"
pages_build_output_dir = ".output/public"

[[kv_namespaces]]
binding = "SESSIONS"
id = "xxxx"

15.5 静的サイト

nuxt generate ですべてのページを HTML 化する。API ルートは実行できなくなるため、静的ホスティング(GitHub Pages, Netlify, Cloudflare Pages 静的モード、S3 + CloudFront)に最適。

15.6 環境変数とシークレット

前述の通り、ランタイムに変わる値は NUXT_* プレフィックスで注入する。ビルド時定数は .env でも構わないが、コンテナイメージには焼かないこと。

15.7 ヘルスチェックと可観測性

小さなエンドポイント server/api/healthz.ts を用意し、K8s の liveness/readiness に使う。

export default defineEventHandler(() => ({ ok: true, ts: Date.now() }))

OpenTelemetry は @nuxt/opentelemetry(コミュニティ)や、Nitro プラグインで @opentelemetry/sdk-node を直接初期化して使うのが現実的。

16. Nuxt DevTools、テスト、開発体験

16.1 Nuxt DevTools

開発中、画面下部にフローティングで表示される DevTools パネル。以下の情報をリアルタイムで確認できる。

  • ルート、レイアウト、ページメタ
  • コンポーネントツリーと props
  • Auto-imports の解決結果
  • Pinia ストアの状態
  • サーバールートの一覧とテスト実行
  • モジュールとフック
  • Payload の中身
  • バンドルアナライザ(Vite プラグイン連携)

インストール不要で、nuxt.config.tsdevtools.enabledtrue(既定)のままにしておけば使える。

16.2 Vitest による単体テスト

pnpm add -D vitest @nuxt/test-utils @vue/test-utils happy-dom
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
  test: { environment: 'nuxt' }
})
// tests/components/AppHeader.nuxt.test.ts
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import AppHeader from '~/components/AppHeader.vue'

describe('AppHeader', () => {
  it('renders site title', async () => {
    const wrapper = await mountSuspended(AppHeader)
    expect(wrapper.text()).toContain('My App')
  })
})

mountSuspended は Suspense + Nuxt ランタイムを考慮して非同期的にマウントできる。@nuxt/test-utils には setup() という E2E 用の API もあり、本物の Nuxt サーバーを起動して fetch できる。

16.3 Playwright による E2E

// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
  webServer: {
    command: 'pnpm dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI
  },
  testDir: 'tests/e2e'
})
// tests/e2e/home.spec.ts
import { test, expect } from '@playwright/test'
test('home renders', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByRole('heading', { level: 1 })).toHaveText('最新の記事')
})

16.4 ESLint と整形

@nuxt/eslint モジュールは、ESLint Flat Config を自動生成し、Nuxt 固有のルール(nuxt/nuxt-config-keys-order など)を含める。

modules: ['@nuxt/eslint']

これを有効にすると、eslint.config.mjs が生成され、pnpm lint で即チェックできる。

16.5 デバッグ

  • pnpm dev --inspect で Node インスペクタが起動。VS Code の Attach で接続できる。
  • useNuxtApp() を DevTools コンソールで $nuxt として触れる。
  • Nitro の debug: true でサーバーログを詳細化。

17. よくある落とし穴とベストプラクティス、まとめ

17.1 落とし穴一覧

症状原因対策
SSR 時に window is not definedクライアント API をサーバー側で参照if (import.meta.client) で保護、または *.client.vue
useState の値が次リクエストに残る状態がモジュールスコープ必ず useState or 関数でラップ
useFetch が再取得しないURL が文字列直指定で変化検知できない関数化し watch を明示
useAsyncData が二重に走るキーが未指定で自動キーがユニークにkey を明示
definePageMeta で変数参照できないコンパイル時マクロのため静的解析リテラル値のみ使用
Cookie 認証で /api/me が 401SSR 時にリクエスト Cookie を引き回していないuseRequestHeaders(['cookie'])
本番で環境変数が反映されないビルド時に値が凍結されたNUXT_* 環境変数はランタイム読み込み
Edge 向けビルドで fs エラーNode 専用 API を使っているサーバー依存を切り分け、Edge では外す

17.2 設計ベストプラクティス

  1. ディレクトリ規約を徹底するapp/, server/, shared/ のレイヤリングをチーム全員で共有する。
  2. 型はサーバーハンドラから流すdefineEventHandler の戻り値に型を必ず付け、#shared/types を中心にドメイン型を集約。
  3. SSR と CSR の境界を意識する — ブラウザ専用ライブラリはプラグインで .client 限定にする。
  4. ルートルールを先に決める — ページごとにどう描画するか(prerender / isr / ssr / spa)を最初に設計する。
  5. 秘匿情報は public に出さないruntimeConfig.public を使うのは、本当にクライアントに見せて良い値だけ。
  6. モジュールで再利用 or レイヤーで継承 — プロジェクトをまたいだ共通化は、モジュール化 → Nuxt Layer 化 の順で検討。
  7. テストは「コンポーネント」「コンポーザブル」「E2E」の三層で — Vitest + @nuxt/test-utils + Playwright を併用。
  8. DevTools を常用する — バンドル肥大、不要なクライアント実行、プラグインの競合を常時可視化。

17.3 今後の展望

  • Nuxt 4 の安定化app/ 構造、shared/ エイリアス、既定の compatibilityDate によって、プロジェクトの「将来における壊れにくさ」が大きく改善した。
  • Server Components の進化 — 島アーキテクチャ (*.island.vue) と完全な Server Components の境界が整理されつつある。
  • H3 v2、Nitro v3 — Web 標準 Request/Response への完全移行が進み、Edge ランタイムでの動作がさらに改善。
  • AI / Agent との統合useScript を起点に、AI SDK や Vercel AI SDK をフロントに組み込みやすくなっている。

17.4 まとめ

Nuxt は、Vue の上で最短距離で Production-grade なアプリを組むための統合フレームワーク である。ファイルベースルーティングと Auto-imports で記述量を減らし、Nitro によりデプロイターゲットを抽象化し、Route Rules と Server Islands でパフォーマンスを細かく制御できる。runtimeConfigapp.config.tsshared/server/ の役割分担を正しく理解し、TypeScript と連携することで、エンドツーエンド型安全な開発体験が手に入る。

本稿で扱った範囲は「全体像」に留まるが、個別のトピック(@nuxt/content で作る CMS、@sidebase/nuxt-auth で作る認証、Cloudflare Workers へのデプロイ、Pinia での大規模ストア設計など)は、いずれもこの全体像の上に自然に積み上がる。Nuxt の強みはまさにその「積み上がりやすさ」である。

次に進むなら、nuxi init で雛形を作り、@nuxt/content@nuxt/ui だけで小さなブログを立ち上げ、routeRules で Prerender と ISR を切り替え、Vercel にデプロイしてみるといい。Nuxt が「規約で回る」ことの快適さは、一度触れば他のフレームワークに戻りにくいほど強烈である。