こんにちは、かつコーチです。
前回は useFetch ・ useAsyncData を使ってAPIからデータを取得する方法を解説しました。
今回はその取得先、つまり「APIそのもの」をNuxtアプリの中に作る方法を扱います。
「バックエンドを別に用意するのが面倒」「ちょっとしたAPIをNuxtだけで完結させたい」という人に、ぜひ読んでほしい内容です。
server/apiとは?
Nuxtに内蔵されたサーバー機能
Nuxtには server/api というディレクトリがあり、ここにファイルを置くだけでAPIエンドポイントを作れます。
これはNitro(Nitro:Nuxtが内部で使っているサーバーエンジン。Node.jsだけでなくVercelやCloudflare Workersなど様々な環境で動くように設計されています)という仕組みによって実現されています。
別途Expressサーバーを立てたり、Laravelのようなバックエンドを用意したりしなくても、フロントエンドと同じプロジェクトの中に簡易的なAPIを持てるのが最大のメリットです。
なぜNuxtの中にAPIを作るのか
小規模なアプリや、ちょっとしたデータの中継処理(外部APIキーを隠したい場合など)では、わざわざ別サーバーを立てるのはオーバースペックなことがあります。
server/api を使えば、フロントとバックエンドのコードを同じリポジトリ・同じTypeScriptの型で管理できるので、開発のスピードが上がります。
基本の書き方 / 実装手順
手順1:GETエンドポイントを作る
server/api 配下にファイルを置くと、そのファイル名がそのままURLになります。
// server/api/posts.get.ts
export default defineEventHandler(() => {
return [
{ id: 1, title: "Nuxtことはじめ" },
{ id: 2, title: "server/apiの基本" },
]
})
defineEventHandler はイベントハンドラ(イベントハンドラ:HTTPリクエストを受け取り、レスポンスを返す関数のこと)を定義するための関数です。
ファイル名を posts.get.ts のように .get.ts にすると、GETメソッドのリクエストだけを受け付けるエンドポイントになります。
このAPIには /api/posts というURLでアクセスできます。
手順2:クエリパラメータ・パスパラメータを受け取る
動的なURL(例:/api/posts/1)を扱いたい場合は、[id] という形でファイル名を作ります。
// server/api/posts/[id].get.ts
interface Post {
id: number
title: string
body: string
}
const posts: Post[] = [
{ id: 1, title: "Nuxtことはじめ", body: "本文1" },
{ id: 2, title: "server/apiの基本", body: "本文2" },
]
export default defineEventHandler((event) => {
const id = Number(getRouterParam(event, "id"))
const post = posts.find((p) => p.id === id)
if (!post) {
throw createError({
statusCode: 404,
statusMessage: "投稿が見つかりません",
})
}
return post
})
getRouterParam でURLの中の [id] 部分を取得し、createError を使うことで404エラーを正しいステータスコードとともに返せます。
クエリパラメータ(?keyword=nuxt のような形式)を取りたい場合は getQuery(event) を使います。
// server/api/posts/search.get.ts
export default defineEventHandler((event) => {
const query = getQuery<{ keyword?: string }>(event)
return { keyword: query.keyword ?? "" }
})
手順3:POSTでデータを受け取る
フォーム送信など、リクエストボディを受け取りたい場合は readBody を使います。
// server/api/posts.post.ts
interface CreatePostInput {
title: string
body: string
}
export default defineEventHandler(async (event) => {
const input = await readBody<CreatePostInput>(event)
if (!input.title) {
throw createError({
statusCode: 400,
statusMessage: "タイトルは必須です",
})
}
// 本来はここでDBへの保存処理を行う
return { id: 3, ...input }
})
ファイル名を posts.post.ts にすることで、POSTメソッドのリクエストだけを受け付けます。
手順4:フロントエンドから呼び出す
作成したAPIは、前回解説した useFetch や $fetch からそのまま呼び出せます。
<!-- pages/posts/index.vue -->
<script setup lang="ts">
interface Post {
id: number
title: string
}
const { data: posts } = await useFetch<Post[]>("/api/posts")
async function createPost(): Promise<void> {
await $fetch("/api/posts", {
method: "POST",
body: { title: "新しい投稿", body: "本文です" },
})
}
</script>
同じプロジェクトの中で完結しているため、外部APIのURLをいちいち環境変数で管理する必要もありません。
つまずきやすい設定・注意点
ファイル名のサフィックス(.get.ts ・ .post.ts など)を付け忘れると、すべてのHTTPメソッドを受け付けるエンドポイントになってしまいます。
意図せず全メソッド受け付けの状態になっていると、GETのつもりで叩いたURLに誰かがPOSTを送れてしまう、というセキュリティ上のリスクにつながります。
用途を絞りたいエンドポイントでは、サフィックスを省略せずに明示するようにしましょう。
よくあるつまずきポイント・エラー対処
Before/After:readBodyを呼ぶ位置でハマった話
私が実際につまずいたのは、POSTのAPIを作ったときに次のようなエラーに遭遇したことでした。
❌ Before:awaitを付け忘れる
// server/api/posts.post.ts
export default defineEventHandler((event) => {
const input = readBody(event) // awaitを付け忘れている
return { received: input }
})
このコードを実行すると、input の中身がリクエストボディではなく Promise オブジェクトそのものになってしまい、フロントエンド側で受け取った received の値が空のオブジエクトのように見えるという不可解な状態になりました。
readBody は非同期関数なので、await を付けないと中身がまだ解決されていないPromiseのまま扱われてしまうのが原因です。
✅ After:readBodyにawaitを付ける
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
const input = await readBody(event)
return { received: input }
})
defineEventHandler のコールバック自体を async にして、readBody の前に await を付けることで、正しくリクエストボディの中身を取得できるようになりました。
この手のミスはエラーメッセージが出ないぶん気づきにくいので、非同期処理には必ず await を付ける癖をつけておくのがおすすめです。
応用・一歩先の使い方
共通処理をミドルウェア的にまとめる
複数のAPIで共通のチェック(認証トークンの確認など)を行いたい場合は、server/middleware ディレクトリにファイルを置くことで、すべてのAPIリクエストの前に処理を差し込めます。
// server/middleware/logger.ts
export default defineEventHandler((event) => {
console.log(`[${event.method}] ${event.path}`)
})
このファイルは server/api 配下のすべてのリクエストに対して自動的に実行されるため、ログ出力や簡易的な認証チェックをまとめておくのに便利です。
外部APIキーを隠す中継役として使う
server/api はブラウザからは見えないサーバー側で動くため、外部APIの秘密鍵を扱うのにも向いています。
// server/api/weather.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery<{ city?: string }>(event)
const apiKey = useRuntimeConfig().weatherApiKey // ブラウザには見えない
return await $fetch("https://api.example.com/weather", {
query: { city: query.city, key: apiKey },
})
})
このように、ブラウザから直接叩くと秘密鍵が漏れてしまう外部APIを server/api 経由で中継することで、フロントエンドのコードには一切キーを書かずに済みます。
まとめ
この記事のポイント
server/apiにファイルを置くだけで、Nuxt内蔵のNitroエンジンによってAPIエンドポイントが作られるdefineEventHandlerでイベントハンドラを定義し、.get.ts・.post.tsなどでメソッドを絞るgetRouterParam・getQuery・readBodyでパラメータやリクエストボディを取得できるreadBodyは非同期関数なのでawaitの付け忘れに注意する- 外部APIの秘密鍵を隠す中継役としても活用できる
次に読むべき記事
自前のAPIとフロントエンドを組み合わせられるようになったら、次はアプリ全体で使う状態(ログイン情報やカート内容など)をどう管理するかが気になってくるはずです。
→ 次の記事:Piniaで状態管理を行う(Nuxtでの導入方法)