Nuxt の基本 — ディレクトリ規約とサーバー API
18 章で useFetch と $fetch を扱いましたが、それが動く土台には触れませんでした。この章はその土台 — ディレクトリの規約とファイル名から決まる仕組み — を扱います。
Nuxt は Vue に SSR・ルーティング・サーバー API を足したフレームワークです。設定ファイルに書く代わりにファイルをどこに置くかで挙動が決まるのが特徴で、規約を知らないと動かない部分が多くあります。
この章で学ぶこと
npm create nuxt@latestが何を聞き、何を作るかapp/配下のディレクトリと、server//public//shared/の役割- ファイル名から URL が決まる仕組みと、動的セグメント・キャッチオールの優先順位
- レイアウトの当て方と
definePageMeta。この macro に書けるものと、書くとアプリ全体が落ちるもの server/apiのファイル名でメソッドを絞る書き方と、一致しなかったときに返るもの
プロジェクトを作る
npm create nuxt@latest my-app
テンプレート・パッケージマネージャ・依存のインストール・git の初期化・追加モジュールを指定できます。すべてコマンドラインから渡せます (--template / --packageManager / --install / --gitInit / --modules)。非対話の端末で回すと --template と --gitInit を要求されるので、この 2 つだけは既定値を持ちません。
テンプレートは create-nuxt 3.37.0 の時点で 5 つです。
| テンプレート | 中身 |
|---|---|
minimal | app.vue だけの最小構成 |
content | Nuxt Content を組み込んだコンテンツ主体のサイト向け |
ui | Nuxt UI 入り |
module | Nuxt モジュールを作るための雛形 |
v5-nightly | Nuxt 5 の nightly 版 |
minimal が作るのはこれだけです。
my-app/
├── app/
│ └── app.vue
├── public/
│ ├── favicon.ico
│ └── robots.txt
├── nuxt.config.ts
├── package.json
├── tsconfig.json
├── README.md
└── .gitignore
app/ は作られますが、中身は app.vue だけです。pages/ などは必要になったときに自分で作ります。
tsconfig.json は自分では設定を持たず、.nuxt/ に生成される 4 つ (app / server / shared / node) を参照するだけです。生成は package.json の postinstall に入っている nuxt prepare が行います。node_modules を消したら npm install を通さないと型が壊れます。
ディレクトリの決まり
Nuxt 4 ではアプリのコードが app/ の下にまとまります。置ける場所は次の 8 つです。
| ディレクトリ | 用途 |
|---|---|
pages/ | ファイル名から URL が決まるページ |
components/ | 自動で import されるコンポーネント |
composables/ | 自動で import される Composable |
layouts/ | ページを包むレイアウト |
middleware/ | 遷移の前に走らせるコード |
plugins/ | アプリ生成時に走らせるコード |
utils/ | 自動で import される関数 |
assets/ | ビルドツールが処理する資産 |
直下には app.vue (ルートコンポーネント) / error.vue (エラーページ) / app.config.ts (リアクティブな設定) を置きます。
app/ の外にも決まった場所があります。最初に覚えるのは次の 4 つで、ほかに modules/ (自作モジュール) / layers/ / content/ / test/ があります。
| 場所 | 用途 |
|---|---|
server/ | api/ routes/ middleware/ plugins/ utils/ を持つサーバー側のコード |
public/ | 加工せずそのまま配信するファイル (robots.txt など) |
shared/ | Vue 側とサーバー側の両方から使うコード |
nuxt.config.ts | 設定 (root に置く) |
Nuxt 3 では pages/ や components/ をリポジトリの root 直下に置いていました。Nuxt 4 ではこれらが app/ の下に移っています。古い記事のパスをそのまま使うと認識されません。
ページとルーティング
app/pages/ にファイルを置くと、その名前が URL になります。
app/pages/
├── index.vue → /
├── about.vue → /about
├── posts/
│ └── [id].vue → /posts/1
└── [...slug].vue → 上のどれにも一致しなかった経路
[id].vue の id は route.params.id で読めます。[...slug].vue は残り全部を受け取り、route.params.slug に配列で入ります。具体的なルートのほうが優先されるので、/posts/1 は [...slug].vue ではなく posts/[id].vue が受け取ります。
ページを表示する枠は app.vue に書きます。
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<NuxtPage /> が URL に対応するページに置き換わり、<NuxtLayout> がその外側をレイアウトで包みます。
一覧ページはこうなります。
<script setup lang="ts">
interface Post {
id: number
title: string
}
const { data: posts, status, error, refresh } = await useFetch<Post[]>('/api/posts')
</script>
<template>
<div>
<p v-if="status === 'pending'">読み込み中</p>
<p v-else-if="status === '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>
<button type="button" @click="refresh()">再取得</button>
</div>
</template>
useFetch の status は 'idle' / 'pending' / 'success' / 'error' の 4 つです。サーバー側の描画では解決を待つので、初回の HTML には結果が入った状態で届きます。 status を見るのはクライアント側の遷移や再取得のためです。
クライアント側の遷移を待たせたくないなら useLazyFetch を使います。useFetch に lazy: true を付けたのと同じで、signature も変わりません。効くのはクライアント側だけで、サーバー側では同じく待ちます (初回の HTML には status: success とデータが入ります)。この名前はコンパイラが変換する予約語なので、自作の関数に同じ名前を付けないでください。
レイアウト
app/layouts/default.vue を置くと、レイアウトを指定していないページがこれで包まれます。中身は <slot /> の位置に入ります。
<template>
<div>
<header>
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
</nav>
</header>
<main>
<slot />
</main>
</div>
</template>
別のレイアウトを当てるには、ページ側で definePageMeta に名前を渡します。
<script setup lang="ts">
definePageMeta({ layout: 'alt' })
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
</script>
<template>
<article>
<h1>{{ post?.title }}</h1>
</article>
</template>
definePageMeta に書けるもの
definePageMeta は app/pages/ のページ専用のコンパイラマクロです。layout のほかに middleware / key / keepalive / validate / name / path / alias / redirect / scrollToTop / 各種トランジション、それに好きな名前のカスタムキーを渡せます。
definePageMeta は、引数と、それが参照しているローカルの宣言をまとめてモジュールのトップレベルへ持ち上げます。マクロが一律にこうするわけではありません — defineProps はローカルの参照をコンパイル時に弾きます。defineModel も原則そうですが、10 章で見たとおり spread を挟むと検査を素通りして実行時の ReferenceError になります。
持ち上がるのは変数宣言と関数宣言だけです。この 2 つなら参照して構いません。逆に class 宣言は運ばれないので、参照すると ReferenceError: ... is not defined でアプリ全体が 500 になります (診断コードは付きません)。
function pickLayout() {
return 'alt'
}
const chosen = pickLayout()
definePageMeta({ layout: chosen })
持ち上げても解決できないものは、止まる場所が 2 通りに分かれます。
1 つは Nuxt のコンテキストを要求するものです。持ち上げ先にはそのコンテキストが無いので、useRoute() のような Composable は NUXT_E1001 を投げます。
// これを書くとアプリ全体が動かなくなる
const route = useRoute()
definePageMeta({ layout: route.query.l === 'alt' ? 'alt' : 'default' })
ビルドは通ります。 落ちるのは実行時で、しかもそのページだけでなくアプリ全体が 500 になります。ページを 1 つ足しただけでトップページまで開けなくなるので、原因を追いにくい種類の壊れ方です。
もう 1 つは await を含む変数です。こちらはビルドの時点で止まります。
[NUXT_B4002] An `await` expression is used in a variable referenced by `definePageMeta`, which runs synchronously.
╰▶ fix: Move the `await` outside of variables referenced in `definePageMeta`, or use a static value instead
理由はコンテキストではなく、definePageMeta が同期に走ることです。診断の文面どおりで、await Promise.resolve('alt') のように Nuxt と無関係な待ちでも同じく落ちます。useFetch の結果でレイアウトを決めようとするとこれに当たります。
条件でレイアウトを変えたいなら、definePageMeta には固定値を書き、切り替えは <NuxtLayout name="..."> を使うか、レイアウト側で分岐します。
サーバー API
server/api/ にファイルを置くと、そのままエンドポイントになります。defineEventHandler の戻り値が JSON として返ります。
export default defineEventHandler(() => {
return [
{ id: 1, title: '最初の記事' },
{ id: 2, title: '次の記事' },
]
})
ファイル名の .get / .post などの suffix で HTTP メソッドを絞れます。 suffix を付けなければすべてのメソッドに応えます。同じパスでメソッドごとに別ファイルを置けます。
export default defineEventHandler(async (event) => {
const body = await readBody<{ title?: string }>(event)
if (!body?.title) {
throw createError({ statusCode: 400, message: 'title が要ります' })
}
return { id: 3, title: body.title }
})
動的セグメントはページと同じ [id] の形で、getRouterParam で読みます。
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({ statusCode: 400, message: 'id が要ります' })
}
return { id: Number(id), title: `記事 ${id}` }
})
メソッドが一致しないと何が返るか
405 は返りません。 一致するハンドラが無いので、そのリクエストは通常のページ解決へ落ちます。つまり返るものはキャッチオールページがあるかどうかで変わります。
| キャッチオール | POST /api/posts (GET 専用のとき) |
|---|---|
| 無い | 404 |
[...slug].vue がある | そのページの HTML が 200 で返る (route.params.slug は api/posts) |
後者が厄介です。$fetch は HTML を文字列としてそのまま返し、例外を投げません。 200 なのでエラー処理にも入らず、data に HTML が入ったまま処理が進みます (素の fetch(...).json() なら例外になります)。キャッチオールページを置くなら、/api/ 以下を弾く分岐を入れるか、メソッドの取り違えを疑ってください。
まとめ
npm create nuxt@latest <name>はテンプレート・パッケージマネージャ・インストール・git の初期化・モジュールを受け取ります。コマンドラインで渡さなかったものは対話で補われ、既定値を持たないのは--templateと--gitInitです。テンプレートは create-nuxt 3.37.0 の時点でminimal/content/ui/module/v5-nightlyの 5 つminimalが作るのはapp/app.vue/public//nuxt.config.ts/package.json/tsconfig.json/README.md/.gitignoreだけです。tsconfig.jsonは.nuxt/の 4 つを参照するだけなので、postinstallのnuxt prepareが走っていないと型が壊れます- アプリのコードは
app/の下に置きます (pages/components/composables/layouts/middleware/plugins/utils/assetsの 8 つ)。直下にapp.vue/error.vue/app.config.ts。app/の外はserver//public//shared//nuxt.config.tsが中心です。Nuxt 3 は root 直下だったので、古い記事のパスは通りません - ファイル名から URL が決まります。
[id].vueが動的セグメント、[...slug].vueがキャッチオールで、具体的なルートのほうが優先されます useFetchのstatusは'idle'/'pending'/'success'/'error'の 4 つです。サーバー側の描画では解決を待つので、初回の HTML には結果が入っています。useLazyFetchが待たせないのはクライアント側の遷移だけで、サーバー側では同じく待ちます (予約語なので自作関数に同じ名前を付けないこと)- レイアウトは
app/layouts/に置き、<slot />の位置に中身が入ります。切り替えはdefinePageMeta({ layout: '...' })です definePageMetaは引数と、それが参照するローカルの宣言をまとめてトップレベルへ持ち上げます (マクロ一般の性質ではありません)。持ち上がるのは変数宣言と関数宣言だけで、class宣言は運ばれずReferenceErrorになります- 持ち上げても解決できないものは止まる場所が分かれます。Nuxt のコンテキストを要求する Composable は実行時に
NUXT_E1001でアプリ全体を 500 にします (ビルドは通ります)。awaitを含む変数はNUXT_B4002でビルドを止めます — 同期に走るためで、Nuxt と無関係なawaitでも同じです server/api/のファイル名の.get/.postsuffix でメソッドを絞れます。suffix 無しは全メソッド。動的セグメントは[id]でgetRouterParamから読みます- メソッドが一致しないと 405 ではありません。 キャッチオールページが無ければ 404、
[...slug].vueがあるとそのページの HTML が 200 で返ります。$fetchはその HTML を文字列として返して例外を投げないので、静かに壊れます
- Nuxt — Installation — 前提とプロジェクトの作り方
- Nuxt — Directory Structure —
app/とserver/の一覧 - Nuxt —
definePageMeta— 渡せるキーの型 - Nuxt — Server Directory — メソッド suffix と動的ルート
次に読む
- 認証とデプロイ — トークンの置き場所と本番の設定