Vue エコシステムのフルスタックメタフレームワーク Nuxt は、2025 年 7 月の 4.0 安定版で大きく生まれ変わりました。プロジェクトルートに散らばっていた設定・ソース・サーバーコードが app/ と server/ に明確に分離され、useFetch / useAsyncData の挙動も同キー共有・shallowRef 既定・undefined 既定へと最適化。TypeScript プロジェクトが「アプリ / サーバー / 共有 / ビルダー」の 4 つに自動分割され、型推論の精度が劇的に上がりました。
React + Next.js、Svelte + SvelteKit と並ぶ 3 大フルスタック選択肢のひとつとして、Nuxt 4 は「Vue 3 Composition API の書きやすさ+Nitro による任意プロバイダへのデプロイ+Auto-imports による最小ボイラープレート」という独自ポジションを強化しました。日本語の Vue コミュニティも依然強く、2026 年はエンタープライズ領域で採用が伸びています。
この記事では Nuxt 4.x を前提に、新しい app/ ディレクトリ構造、Auto-imports、Pages と File-based Routing、Nitro サーバールート、改善された useFetch、Layouts / Middleware、Nuxt Modules、Pinia での状態管理、TypeScript プロジェクト分割、各プロバイダへのデプロイ、compatibilityVersion 4 による段階移行、Nuxt 3 → 4 の破壊的変更と自動移行 codemod、落とし穴まで一気通貫で解説します。
- 2026 年 4 月時点の Nuxt バージョン整理
- Nuxt とは ── Vue フルスタックメタフレームワーク
- プロジェクト作成と初期構造
- Pages と File-based Routing
- Auto-imports ── import 不要の開発体験
- useFetch / useAsyncData ── Nuxt 4 で改善されたデータフェッチ
- Nitro サーバールート ── server/api と server/routes
- Layouts / Middleware / Plugins
- Nuxt Modules ── エコシステム
- TypeScript プロジェクト分割(Nuxt 4 の新機能)
- デプロイ ── 任意のプロバイダへ同じコード
- Nuxt 3 → 4 移行手順
- 落とし穴と注意点
- よくある質問
- まとめ
2026 年 4 月時点の Nuxt バージョン整理
| リリース | 時期 | 主なハイライト |
|---|---|---|
| Nuxt 3.x | 2022 年〜 | Vue 3 + Vite + Nitro の全面書き換え、compatibilityVersion: 4 で v4 挙動を事前テスト可能 |
| Nuxt 4.0 | 2025 年 7 月 | app/ ディレクトリ化、useFetch / useAsyncData 同キー共有、data が shallowRef に、null 既定 → undefined、TypeScript プロジェクト分割、SPA loading template 外側配置 |
| Nuxt 4.1〜4.x | 2025 年末〜2026 年 | CLI 高速化、useFetch 型推論改善、Nitro Tasks 改善、Hybrid rendering strategies 拡張 |
| 推奨 | 2026 年 4 月 | Nuxt 4.x 系。新規プロジェクトは最初からこの系列 |
npx codemod nuxt/4/migration-recipe で自動化でき、手作業が最小になるよう設計されています。Nuxt とは ── Vue フルスタックメタフレームワーク
Nuxt は Vue 3 上に構築されたフルスタックメタフレームワークです。フロントエンドの Vue アプリに加え、API ルート(Nitro)、SSR / SSG / SPA / Edge レンダリング、ファイルベースルーティング、Auto-imports、モジュールエコシステムを 1 パッケージで提供します。
| 機能 | 内容 |
|---|---|
| Vue 3 統合 | Composition API / script setup / reactivity をそのまま使える。詳細は TypeScript × Vue 3 Composition API 完全ガイド |
| File-based Routing | app/pages/**/*.vue が自動的にルートに。動的・ネスト・キャッチオール対応 |
| Auto-imports | components / composables / utils / Vue API / Nuxt API が import 不要で使える |
| Nitro サーバー | server/api/**/*.ts が API ルートに。任意のプロバイダ(Node / Vercel / Cloudflare / Bun / Deno / Netlify / AWS Lambda)へ同じコードでデプロイ |
| Rendering Modes | SSR / SSG / SPA / Hybrid(ルート単位で選択可能) |
| Modules | 200+ の公式 / コミュニティモジュール(@nuxtjs/tailwindcss、@pinia/nuxt、@sidebase/nuxt-auth など) |
プロジェクト作成と初期構造
# 対話形式で雛形生成(Bun / pnpm / npm / yarn から選択) npx nuxi@latest init my-app # ? Which package manager would you like to use? pnpm # ? Initialize git repository? Yes # ? Install dependencies? Yes cd my-app pnpm dev # http://localhost:3000 pnpm build # .output/ にビルド pnpm preview # 本番モードで起動 # モジュール追加(例: Tailwind CSS) npx nuxi module add @nuxtjs/tailwindcss
Nuxt 4 の新しいディレクトリ構造
my-app/ ├── app/ ← アプリケーションコードがここに集約(Nuxt 4 の新規則) │ ├── assets/ │ ├── components/ ← Auto-import 対象 │ ├── composables/ ← Auto-import 対象 │ ├── layouts/ │ ├── middleware/ │ ├── pages/ ← File-based Routing │ ├── plugins/ │ ├── utils/ ← Auto-import 対象 │ ├── app.vue ← ルートコンポーネント │ ├── app.config.ts ← ランタイム設定 │ └── error.vue ├── server/ ← サーバー / API コード(Nitro) │ ├── api/ │ ├── routes/ │ ├── middleware/ │ └── utils/ ├── shared/ ← app/ と server/ の両方で使う型や定数 ├── public/ ← そのまま配信される静的ファイル ├── modules/ ← ローカルの Nuxt Module ├── nuxt.config.ts ├── package.json └── tsconfig.json
app/ に分けたか: Nuxt 3 ではプロジェクトルートに components/ / pages/ / server/ / node_modules/ / 設定ファイル群が並列で存在し、ファイルウォッチャーがルート全体を監視するためWindows や WSL で劇的に遅くなる問題がありました。Nuxt 4 の app/ 分離はこの問題を根本解決し、IDE のインデックス速度も向上しています。Pages と File-based Routing
app/pages/
├── index.vue → /
├── about.vue → /about
├── posts/
│ ├── index.vue → /posts
│ ├── [id].vue → /posts/:id
│ └── [...slug].vue → /posts/* (catch-all)
├── users/
│ ├── [id]/
│ │ ├── index.vue → /users/:id
│ │ └── settings.vue → /users/:id/settings
└── (auth)/ ← グループ(URL に含まれない)
├── login.vue → /login
└── register.vue → /register
<script setup lang="ts">
// useRoute で params 取得(Auto-import なので import 不要)
const route = useRoute();
const id = computed(() => String(route.params.id));
// useFetch でサーバールートからデータ取得
const { data: post, pending, error } = await useFetch(`/api/posts/${id.value}`);
// <head> を設定(useHead も Auto-import)
useHead({
title: () => post.value?.title ?? "読み込み中...",
meta: [{ name: "description", content: () => post.value?.excerpt ?? "" }],
});
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<time>{{ new Date(post.publishedAt).toLocaleDateString() }}</time>
<div v-html="post.html" />
</article>
<p v-else-if="pending">読み込み中...</p>
<NuxtLink v-else to="/posts">一覧へ</NuxtLink>
</template>
Auto-imports ── import 不要の開発体験
Nuxt は決まった場所に置かれたコンポーネント・コンポーザブル・ユーティリティを自動で importします。Vue API(ref / computed / watch)・Nuxt API(useFetch / useHead / useState)も import 不要です。
// ファイル名がそのままコンポーザブル名になる(useCounter)
export const useCounter = (initial = 0) => {
const count = ref(initial);
const increment = () => (count.value += 1);
const decrement = () => (count.value -= 1);
return { count, increment, decrement };
};
<script setup lang="ts">
const { count, increment } = useCounter(10);
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
コンポーネントの Auto-import
<!-- 別ファイルから <UiButton> として使える(ディレクトリ名 + ファイル名) --> <template> <UiButton variant="primary" @click="save">保存</UiButton> </template>
nuxt.config.ts で imports: { autoImport: false } を設定すると明示 import 強制モードになります。Auto-import を使いつつ特定コンポーザブルだけ除外するなど細かな制御もできます。useFetch / useAsyncData ── Nuxt 4 で改善されたデータフェッチ
Nuxt 4 では useFetch と useAsyncData が 4 つの重要な変更を受けました。
| 変更点 | 内容 |
|---|---|
| 同キー共有 ref | 同じキーを使う複数コンポーネントが同一の data / error / status を共有する |
| shallowRef 既定 | data は shallowRef 型に。深い値の変更を検知したい場合は { deep: true } を付ける |
| 既定値が undefined | 初期状態の data / error が null → undefined に変更 |
| dedupe オプション | true / false → "cancel" / "defer" の文字列に変更 |
<script setup lang="ts">
// キーを明示的に渡す(同キー共有の恩恵を受けるため)
const { data: posts, pending, error, refresh } = await useFetch("/api/posts", {
key: "posts-list",
default: () => [], // SSR 中も描画が崩れない
transform: (res: any[]) => res.map(p => ({ ...p, isHot: p.likes > 100 })),
watch: [() => route.query.category], // クエリ変更時に再取得
});
// 深い反応性が必要な場合だけ deep: true
const { data } = await useFetch("/api/deeply-nested", { deep: true });
// dedupe は "cancel" / "defer"
await refresh({ dedupe: "cancel" });
</script>
useAsyncData ── 任意の非同期関数を key 付きで
export const usePost = (id: MaybeRefOrGetter<string>) => {
return useAsyncData(
() => `post-${toValue(id)}`, // キー(同値なら共有される)
() => $fetch(`/api/posts/${toValue(id)}`), // $fetch は Nuxt の fetch ラッパ
{ watch: [() => toValue(id)] },
);
};
$fetch と useFetch の使い分け: $fetch はただの fetch ラッパで、クライアントでもサーバーでも呼べるがリアクティブではない。useFetch は SSR ハイドレーション・キャッシュ共有・watch 再取得を含む Vue のリアクティブ API。ページの初期表示データには useFetch、ボタンクリック時の 1 回限りの API 呼び出しには $fetch を使うのが定石です。Nitro サーバールート ── server/api と server/routes
Nuxt に組み込まれている Nitro は、server/api/** と server/routes/** に置いた TS ファイルを自動的にエンドポイント化します。Node / Vercel / Cloudflare Workers / AWS Lambda / Bun / Deno など任意の実行先へ同じコードでデプロイできるのが最大の利点です。
API エンドポイント(server/api)
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const page = Number(query.page ?? 1);
const posts = await prisma.post.findMany({
take: 20,
skip: (page - 1) * 20,
orderBy: { createdAt: "desc" },
});
return posts;
});
import { z } from "zod";
const Body = z.object({
title: z.string().min(1).max(80),
body: z.string().min(1),
});
export default defineEventHandler(async (event) => {
const raw = await readBody(event);
const parsed = Body.safeParse(raw);
if (!parsed.success) {
throw createError({ statusCode: 400, message: parsed.error.message });
}
const created = await prisma.post.create({ data: parsed.data });
setResponseStatus(event, 201);
return created;
});
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
if (!id) throw createError({ statusCode: 400, message: "id required" });
const post = await prisma.post.findUnique({ where: { id } });
if (!post) throw createError({ statusCode: 404, message: "not found" });
// キャッシュヘッダ
setResponseHeader(event, "Cache-Control", "public, max-age=60");
return post;
});
[name].[method].ts で HTTP メソッド付き、省略すると全メソッド受付。[id] は動的パラメータ、[...slug] はキャッチオール。Nitro が JIT で解析するので手動のルーター定義は不要です。任意 HTML / サーバーレスポンスを返す server/routes
export default defineEventHandler(async (event) => {
const posts = await prisma.post.findMany({ select: { id: true, updatedAt: true } });
setResponseHeader(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.id}</loc>
<lastmod>${p.updatedAt.toISOString()}</lastmod>
</url>`).join("")}
</urlset>`;
});
Layouts / Middleware / Plugins
Layouts
<template>
<div class="app-shell">
<AppHeader />
<main>
<slot /> <!-- ページが埋め込まれる -->
</main>
<AppFooter />
</div>
</template>
<script setup lang="ts">
definePageMeta({ layout: "admin" });
</script>
<template>
<h1>管理画面</h1>
</template>
Middleware(ルート単位の前処理)
export default defineNuxtRouteMiddleware((to) => {
const { user } = useUser(); // 自作 composable
if (!user.value && to.path !== "/login") {
return navigateTo("/login");
}
});
// app/middleware/admin.ts(named middleware)
export default defineNuxtRouteMiddleware((to) => {
const { user } = useUser();
if (!user.value?.isAdmin) {
throw createError({ statusCode: 403, statusMessage: "Forbidden" });
}
});
<script setup lang="ts">
definePageMeta({ middleware: ["auth", "admin"] });
</script>
Plugins
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
export default defineNuxtPlugin(() => {
dayjs.extend(relativeTime);
return {
provide: { dayjs }, // useNuxtApp().$dayjs で使える
};
});
Nuxt Modules ── エコシステム
# Tailwind CSS npx nuxi module add @nuxtjs/tailwindcss # 状態管理(Pinia) npx nuxi module add @pinia/nuxt # コンテンツ(Markdown ベース CMS) npx nuxi module add @nuxt/content # SEO / head 管理 npx nuxi module add @nuxtjs/seo # 画像最適化 npx nuxi module add @nuxt/image # 認証 npx nuxi module add @sidebase/nuxt-auth # i18n npx nuxi module add @nuxtjs/i18n # DB 接続(Drizzle / Prisma は直接 server/ 配下に書くのが一般的) npx nuxi module add @nuxt/db # 実験的統合
Pinia で状態管理
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", () => {
const user = ref<{ id: string; name: string } | null>(null);
const isAuthenticated = computed(() => !!user.value);
async function login(email: string, password: string) {
const res = await $fetch("/api/auth/login", {
method: "POST",
body: { email, password },
});
user.value = res.user;
}
function logout() { user.value = null; }
return { user, isAuthenticated, login, logout };
});
<script setup lang="ts">
const userStore = useUserStore();
await userStore.login("alice@example.com", "pw");
</script>
<template>
<p v-if="userStore.isAuthenticated">{{ userStore.user!.name }}さん</p>
</template>
TypeScript プロジェクト分割(Nuxt 4 の新機能)
Nuxt 4 は .nuxt/ 配下に4 つの別々の TypeScript プロジェクトを自動生成します。これにより、アプリ側で Node 固有型が出る・サーバー側で Vue 型が出る、といった混乱が根本解決しました。
| tsconfig | 対象 |
|---|---|
.nuxt/tsconfig.app.json |
app/ 配下のクライアント / ユニバーサルコード |
.nuxt/tsconfig.server.json |
server/ 配下の Nitro サーバーコード |
.nuxt/tsconfig.shared.json |
shared/ 配下(app/ と server/ の両方から参照) |
.nuxt/tsconfig.node.json |
ビルド時ツール(nuxt.config.ts、ローカル modules) |
{
"references": [
{ "path": "./.nuxt/tsconfig.app.json" },
{ "path": "./.nuxt/tsconfig.server.json" },
{ "path": "./.nuxt/tsconfig.shared.json" },
{ "path": "./.nuxt/tsconfig.node.json" }
],
"files": []
}
デプロイ ── 任意のプロバイダへ同じコード
Nitro は環境変数 NITRO_PRESET(または nitro.preset)で出力ターゲットを切り替えます。同じアプリコードが各プロバイダで動きます。
# Vercel(自動検出) pnpm build vercel deploy # Cloudflare Workers NITRO_PRESET=cloudflare_module pnpm build npx wrangler deploy # Netlify NITRO_PRESET=netlify pnpm build # Node.js サーバー(自前 VPS / Docker) NITRO_PRESET=node-server pnpm build node .output/server/index.mjs # Bun で動かす NITRO_PRESET=bun pnpm build bun .output/server/index.mjs # Deno Deploy NITRO_PRESET=deno-deploy pnpm build deployctl deploy .output/server/index.mjs # AWS Lambda NITRO_PRESET=aws-lambda pnpm build # 完全静的(SSG) pnpm generate # .output/public/ を任意の静的ホスティングへ
nuxt.config.ts で routeRules を使うと、ルート単位で SSR / SSG / ISR / SPA / 404 を指定できます。「トップは SSG、ダッシュボードは SSR、管理画面は SPA」のような混在構成を 1 プロジェクトで実現できます。export default defineNuxtConfig({
routeRules: {
"/": { prerender: true }, // SSG
"/blog/**": { isr: 3600 }, // ISR(1 時間)
"/dashboard/**": { ssr: false }, // SPA
"/admin/**": { ssr: true, robots: false }, // SSR(noindex)
"/api/**": { cors: true, headers: { "Cache-Control": "no-store" } },
},
});
Nuxt 3 → 4 移行手順
移行は「段階的オプトイン → 自動 codemod → 個別修正」の 3 ステップで進めるのが安全です。
Step 1 ── Nuxt 3 で compatibilityVersion 4 を有効化
export default defineNuxtConfig({
future: {
compatibilityVersion: 4, // v4 の挙動を事前テスト
},
// 旧挙動に戻したい項目があれば experimental で個別指定
experimental: {
pendingWhenIdle: true, // pending の旧挙動
normalizeComponentNames: false, // コンポーネント名正規化を無効化
spaLoadingTemplateLocation: "within", // SPA loading の旧位置
},
});
pnpm dev pnpm test pnpm build
Step 2 ── Nuxt 4 にアップグレード
# dedupe で依存重複を整理しつつ v4 安定版へ npx nuxi upgrade --dedupe # ディレクトリ構造を自動で app/ に移行 npx codemod@latest nuxt/4/file-structure # その他の破壊的変更を自動修正 npx codemod@latest nuxt/4/default-data-error-value # data/error 既定値 npx codemod@latest nuxt/4/deprecated-dedupe-value # dedupe: true/false → "cancel"/"defer" npx codemod@latest nuxt/4/shallow-function-reactivity # すべて一度に npx codemod@latest nuxt/4/migration-recipe
Step 3 ── 手動確認が必要な項目
data/errorを参照している個所で初期値null前提のコードをundefinedに対応(if (data.value)で安全)dataがshallowRefになったため、深いオブジェクトを直接書き換えている個所は{ deep: true }を付けるか、data.value = { ...data.value, foo: x }のように差し替えwindow.__NUXT__を使っている場合はuseNuxtApp().payloadに書き換えpages:extendフックを使うモジュールはpages:resolvedに移行- Unhead v2 の API 変更(
vmidプロパティ削除など)への対応
落とし穴と注意点
useFetch の data が shallowRef
Nuxt 4 の data は shallowRef。data.value.items.push(x) のような深い変更は検出されません。素直に data.value = { ...data.value, items: [...data.value.items, x] } と差し替えるか、{ deep: true } オプションを付けます。
既定値が undefined になった副作用
const { data } = await useFetch(...) の直後に data.value.title とやるとTypeError(undefined のプロパティアクセス)。default: () => ({ title: "" }) を指定するか、data.value?.title に変更します。
app/ への移動を忘れる
Nuxt 4 にアップした後、古い位置にある components/ / pages/ は認識されません。codemod で自動移動されなかったファイル(手で置いたもの、動的生成したもの)は、自分で app/ に移動する必要があります。旧構造を維持したい場合のみ srcDir: "." を設定します。
サーバー専用モジュールをクライアントで import する事故
server/ 配下の型やユーティリティは、shared/ に置かない限り app/ からは参照できません(型エラー)。間違えて server/utils/db.ts を app/composables/ から import すると、ビルド時に Prisma や Node 組込みがクライアントに漏れ込む事故になります。TypeScript プロジェクト分割でこの事故が型エラーとして検出されるため、エラーが出たら構造を見直してください。
Auto-import による名前衝突
別ディレクトリに同名のコンポーネント(例: app/components/ui/Button.vue と app/components/form/Button.vue)があると、Auto-import 名は UiButton / FormButton とプリフィックス付きになります。Button で呼ぶと衝突警告が出るので、ディレクトリ構造で命名空間を分けるのが基本です。
SSR と onMounted の混同
SSR 時はサーバーで script setup が実行されますが、onMounted はクライアントでしか走りません。DOM に触れる処理を script setup に直接書くと SSR 時にエラーになります。ブラウザ API を使う処理は必ず onMounted 内、あるいは import.meta.client ガードを使います。
よくある質問
future.compatibilityVersion: 4 で挙動を事前確認してから本移行するのが王道です。server/ 配下で各クライアントを初期化し、API ルートから呼ぶのが基本パターンです。Supabase は @nuxtjs/supabase 公式モジュールで Auth・RLS 統合がしやすく、Drizzle は server/db.ts に Bun.sql や postgres.js を繋いで使います。詳細は Claude Code × Supabase 完全ガイド、TypeScript × Drizzle ORM 完全ガイド を参照。utils/ にはピュア関数のみ、副作用は composables/)、③重要な public API だけ明示 import に切り替え(imports: { dirs: [...], autoImport: "explicit" })。DX の恩恵が大きいため、規約整備で対応するのが現実解です。routeRules なら「トップと記事は SSG、ダッシュボードは SPA」のような混在構成が 1 プロジェクトで可能です。Hybrid が取れるのが Nuxt(と Next.js)の強みです。.vue ファイルを多用しますが、Biome の v2.3 で Vue 実験サポートが入ったため、biome.json の files.includes に **/*.vue を加えるだけで lint / format が動きます。ESLint + Prettier の Vue 設定より桁違いに高速なので、モノレポで採用する価値があります。まとめ
- Nuxt 4 が 2026 年の Vue フルスタック標準。app/ + server/ の明確な分離で IDE / ビルドが高速化
- Auto-imports で components / composables / utils が import 不要。Vue / Nuxt API も自動
- Nitro サーバーで任意プロバイダに同じコードでデプロイ。Vercel / Cloudflare / Netlify / Node / Bun / Deno すべて対応
useFetch/useAsyncDataの改善: 同キー共有 ref / shallowRef 既定 / undefined 既定 / dedupe 文字列化- routeRules でルート単位の SSR / SSG / ISR / SPA 混在が可能
- TypeScript プロジェクト分割(app / server / shared / node)で型事故を静的検出
- Nuxt 3 → 4 移行は compatibilityVersion 4 で事前検証 → codemod で自動修正 → 手動個別対応の 3 ステップ
- 200+ モジュールで Tailwind / Pinia / Content / Image / Auth / i18n / SEO 等が 1 コマンドで統合
Vue 3 Composition API の基礎は TypeScript × Vue 3 Composition API 完全ガイド、React / Svelte / Astro との選定は React 19 完全ガイド・Svelte 5 完全ガイド・Astro 完全ガイド、ランタイム選択は Bun 完全ガイド・Deno 2 完全ガイド、バックエンド API は TypeScript × Hono 完全ガイド、DB は TypeScript × Drizzle ORM 完全ガイド、BaaS は Claude Code × Supabase 完全ガイド、DevTool は Biome 完全ガイド もあわせて、Nuxt 4 を核に据えた 2026 年型 Vue フルスタック構成を組み上げてください。

