はじめに
Next.jsのfetchにはさまざまな方法があります。
- actions.tsにまとめたり
- 直接エンドポイントの
${process.env.NEXT_PUBLIC_URL}/users
を取得したり - RouteHandlersで中間層(route.ts)に関数を書いてそれを
fetch("/api/users")
で取得したり
色々な方法がありすぎてどんな時にどれを使えばいいか忘れがちなので、
備忘録的に、大変雑多にまとめたいと思います。
個人的にはこれで納得していますが、もっとこうした方がいいよ!ということがあればぜひご指摘をお願いいたします。
こちらの記事ではactions.tsについては深く触れていません。
場面ごとに考えてみる。
直接エンドポイントを叩く場合
この時Next.jsは必ずサーバーコンポーネントである必要があります。
基本的にNext.jsは何も宣言しなければサーバーコンポーネントとして扱うという仕様がありますから、先頭にuse client
がないかどうかを確認するだけで十分です。
try{
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users`, {
cache: 'no-store',
})
if(!response.ok){
throw new Error("取得できませんでした")
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResonse.json(
{ error: "エラーが発生しました" },
{ status: 500 }
)
}
use client
がある状態で直接エンドポイントを叩いた場合、ログなどが全てクライアント側に露呈してしまいますので、絶対にやらないよう注意しましょう。
ですがformの初期値設定やPOSTリクエストなど、クライアントサイドでHTTPリクエストをしたい場面は必ず出てきます。そうした際にはRouteHandlersを使用するようにします。
RouteHandlersを使用する場合
RouteHandlersはapi/users/route.ts
など中間層を挟むことでフロント側に見せたくない情報が露呈してしまうことを避けることができます。
useEffect
やuseState
とfetch
を組み合わせる必要がある場合や、
例えばあなたが自身のサービスを開発しているとして、ユーザーが発行するAPIキーのような機密性の高いものを生成してDBに保存したい場合。
これらのようにクライアントにコードを書くとリスクになる場面は多々あります。
そういった時には、一度サーバーコンポーネントである中間層を挟み、DBにPOSTするとセキュアなコードを書くことができると考えています。
例えばこんな感じに。
-
機密情報の取り扱い
- APIキーの生成
- パスワードハッシュの作成
- 認証トークンの管理
-
環境変数の使用
- データベースの接続情報
- 外部APIの認証情報
- サービスの内部設定値
-
データの加工や検証
- ユーザー入力データの無害化
- バリデーションルールの一元管理
- データ形式の変換や正規化
また、RouteHandlersを使用することで、revalidatePathといった優秀なNext.jsのcacheに関するモジュールを使用することができる点も良いです。
注意点として、開発環境でエンドポイントが一つしかないからといって、安易にNEXT_PUBLIC
プレフィックスのついて環境変数をroute.ts
で使うのは絶対にやめましょう。
これはサーバーサイドだけでなく、クライアントサイドからでも参照できてしまいます。
以下はRouteHandlersの一例です。
"use client"
import { useState, useEffect } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
const formSchema = z.object({
username: z.string().min(3, "ユーザー名は3文字以上必要です"),
email: z.string().email("有効なメールアドレスを入力してください"),
})
type FormValues = z.infer<typeof formSchema>
export function UserRegistrationForm() {
const [loading, setLoading] = useState(false)
const [departments, setDepartments] = useState<string[]>([])
// 部署一覧を取得する例
useEffect(() => {
const fetchDepartments = async () => {
try {
const response = await fetch("/api/departments", // 直接エンドポイントをfetchしない
cache: 'no-store' // Next.js14以前の場合
)
if (!response.ok) throw new Error("部署情報の取得に失敗しました")
const data = await response.json()
setDepartments(data)
} catch (error) {
console.error("Error:", error)
}
}
fetchDepartments()
}, [])
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
const onSubmit = async (values: FormValues) => {
setLoading(true)
try {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
})
if (!response.ok) {
throw new Error("ユーザー登録に失敗しました")
}
const data = await response.json()
console.log("登録成功:", data)
form.reset()
} catch (error) {
console.error("Error:", error)
} finally {
setLoading(false)
}
}
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">ユーザー登録</h2>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block mb-2">
ユーザー名:
<input
{...form.register("username")}
className="w-full border rounded p-2"
/>
</label>
{form.formState.errors.username && (
<p className="text-red-500">
{form.formState.errors.username.message}
</p>
)}
</div>
<div>
<label className="block mb-2">
メールアドレス:
<input
{...form.register("email")}
className="w-full border rounded p-2"
/>
</label>
{form.formState.errors.email && (
<p className="text-red-500">
{form.formState.errors.email.message}
</p>
)}
</div>
{departments.length > 0 && (
<div>
<label className="block mb-2">
部署:
<select className="w-full border rounded p-2">
{departments.map((dept) => (
<option key={dept} value={dept}>
{dept}
</option>
))}
</select>
</label>
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600 disabled:bg-gray-400"
>
{loading ? "登録中..." : "登録"}
</button>
</form>
</div>
)
}
import { NextRequest, NextResponse } from "next/server"
import { revalidatePath } from "next/cache"
export async function POST(request: NextRequest) {
try {
const { username, email } = await request.json()
const response = await fetch(`${process.env.API_URL}/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: crypto.randomUUID(),
username,
email,
role: "user", // クライアントに見せる必要のない処理
createdAt: new Date().toISOString()
}),
})
if (!response.ok) {
throw new Error('ユーザー作成に失敗しました')
}
const data = await response.json()
revalidatePath('/users')
return NextResponse.json(data)
} catch (error) {
return NextResponse.json(
{ error: "ユーザー作成中にエラーが発生しました" },
{ status: 500 }
)
}
}
actions.tsを使用する場合。
actions.tsを使用すると、<form action={action} >
のような形で直接使用することができたり、
クライアントコンポーネントの中で'use server'
として非同期的に使用することができます。
なのでフォームの処理などで使用するのがよさそうだという所感です。
詳細はドキュメントを見ていただくのが良いかと思います。
また、getUser.tsのようにファイルごとにまとめることで管理がしやすいという点もあります。
ですが私としてはRESTful APIのような設計のできるRouteHandlersが気にっていますので、現段階ではそれほど深いことは書かないようにします。
この記事のまとめ
-
use client
下では直接APIを叩かない - サーバーコンポーネントの場合、直接叩いても問題はない。ただし、NEXT_PUBLICプレフィックスには注意
- クライアントサイドからPOSTをする場合、中間層RouteHandlersを使用するとセキュア
-
use client
でGETしたい場合も、中間層RouteHandlersを使用するとセキュア - Server Actionsはフォームの処理などと相性が良いさそう