Next.js でアプリを作っていると、外部 API のキーやデータベースの接続情報など「コードに直接書きたくない値」を扱う場面が必ず出てきます。こうした値は環境変数として外部に切り出し、環境ごとに差し替えられるようにするのが定石です。Next.js(App Router)には環境変数の仕組みが標準で組み込まれており、.env.local に書いた値を process.env から参照できます。この記事では、環境変数ファイルの種類と読み込み順、そして最も重要な NEXT_PUBLIC_ プレフィックスの意味、サーバーとクライアントでの扱いの違い、よくあるつまずきの原因までを解説します。
目次
なぜ環境変数を使うのか
API キーやデータベースの接続文字列をソースコードに直接書いてしまうと、二つの問題が起きます。ひとつは、そのコードを Git にコミットした瞬間に秘密の値がリポジトリの履歴に残ってしまうこと。もうひとつは、開発環境と本番環境で違う値を使いたいのに、コードを書き換えないと切り替えられないことです。環境変数を使えば、値そのものはコードの外に置き、実行される環境に応じて読み込む値を差し替えられます。秘匿情報をコードから分離しつつ、ローカル・ステージング・本番で設定を切り替えられるのが最大の利点です。
.env.local に値を書いて参照する
Next.js ではプロジェクトのルートに .env.local というファイルを置き、キー=値 の形式で環境変数を書きます。次のように書くのが基本です。
API_KEY=xxxxxxxxxxxxxxxx DATABASE_URL=postgres://user:password@localhost:5432/mydb
値をクォートで囲む必要は基本的にありません。イコールの前後にスペースを入れないのがルールです。こうして定義した値は、コードの中で process.env.キー名 の形で読み出せます。次は Server Component から API_KEY を使って外部 API を呼ぶ例です。
export default async function Page() {
// .env.local に書いた API_KEY を参照する
const res = await fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
const data = await res.json();
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
ここで大切なのが、.env.local を Git の管理対象から外すことです。このファイルには秘匿値が入るため、リポジトリにコミットしてはいけません。.gitignore に次の一行を加えておきます。create-next-app で作ったプロジェクトなら、最初から .gitignore に .env*.local が含まれています。
# ローカルの秘匿値を含む env ファイルは追跡しない .env*.local
env ファイルの種類と読み込み順
Next.js は .env.local だけでなく、複数の env ファイルを読み分けます。全環境で共通の値は .env、開発時だけ・本番時だけ使いたい値は .env.development や .env.production、そしてローカル環境だけの上書きや秘匿値は .env.local に書く、という使い分けです。同じキーが複数のファイルに定義されている場合は、より優先度の高いファイルの値が採用されます。
| ファイル | 用途 | Git 管理 |
|---|---|---|
.env | 全環境で共通のデフォルト値 | コミットする |
.env.development | 開発時(next dev)だけの値 | コミットする |
.env.production | 本番時(next start)だけの値 | コミットする |
.env.local | ローカル固有の上書き・秘匿値 | コミットしない |
優先順位は、.env.local が最も強く、次に .env.development / .env.production のような環境別ファイル、最後に共通の .env という順で解決されます。つまり同じキーが .env と .env.local の両方にあれば、.env.local の値が勝ちます。共通のデフォルトを .env に置き、手元の環境だけ違う値にしたいときに .env.local で上書きする、という運用がしやすくなっています。なお .env.local は test 環境(NODE_ENV=test)では読み込まれない点だけ覚えておくとよいでしょう。
NEXT_PUBLIC_ プレフィックスの有無で扱いが変わる
Next.js の環境変数でいちばん重要なのが、この NEXT_PUBLIC_ というプレフィックスです。変数名の頭に NEXT_PUBLIC_ を付けるかどうかで、その値がブラウザに届くかどうかが決まります。プレフィックスの有無による違いを整理すると次のようになります。
| 変数の書き方 | 参照できる場所 | ブラウザに送られるか | 向いている用途 |
|---|---|---|---|
API_KEY(プレフィックス無し) | サーバー側のみ | 送られない | API キーなどの秘匿情報 |
NEXT_PUBLIC_API_URL(プレフィックス有り) | サーバー・クライアント両方 | ビルド時に JS へ埋め込まれる | 公開してよい設定値(公開 URL など) |
NEXT_PUBLIC_ を付けない環境変数は、サーバー側(Server Component や Route Handler、Server Action)でのみ参照でき、生成される JavaScript バンドルには一切含まれません。ブラウザに漏れないため、API キーやデータベースのパスワードといった秘匿情報はこちらに置きます。一方 NEXT_PUBLIC_ を付けた変数は、ビルド時にその値が JavaScript のコードへそのまま文字列として埋め込まれます。結果としてブラウザからも参照できますが、裏を返せば誰でもソースを見れば値が分かってしまうということです。だから NEXT_PUBLIC_ には秘密の値を絶対に入れてはいけません。
# サーバー側だけで使う秘匿値(ブラウザには送られない) API_KEY=secret_xxxxxxxx # ブラウザからも使う公開値(JS に埋め込まれる。秘密は入れない) NEXT_PUBLIC_API_URL=https://api.example.com
サーバーコンポーネントとクライアントコンポーネントでの使い分け
ここまでの内容を、実際のコンポーネントの書き方で確認します。Server Component や Route Handler はサーバー上でだけ実行されるので、NEXT_PUBLIC_ の付かない秘匿値をそのまま process.env から読めます。次の Route Handler は API_KEY を使って外部 API を呼び、結果だけをクライアントに返す例です。
import { NextResponse } from "next/server";
export async function GET() {
// サーバー側なので秘匿値をそのまま使える
const res = await fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
const data = await res.json();
// 取得した結果だけをクライアントに返す(API_KEY は返さない)
return NextResponse.json(data);
}
一方、先頭に "use client" を書いたクライアントコンポーネントはブラウザ上で動きます。ここで参照できるのは NEXT_PUBLIC_ の付いた変数だけです。プレフィックス無しの process.env.API_KEY を書いても、その値はバンドルに含まれていないため undefined になります。
"use client";
export function Status() {
// OK: NEXT_PUBLIC_ 付きはクライアントでも参照できる
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// NG: プレフィックス無しはブラウザに存在せず undefined になる
const apiKey = process.env.API_KEY; // -> undefined
return <p>API URL: {apiUrl}</p>;
}
つまり、秘匿情報を扱う処理はサーバー側(Server Component や Route Handler)に閉じ込め、ブラウザから使いたい設定値だけを NEXT_PUBLIC_ として公開する、という役割分担になります。API キーが必要な通信はサーバー側で行い、その結果だけをクライアントに渡すのが安全な設計です。
NEXT_PUBLIC_ の値はビルド時に固定される
NEXT_PUBLIC_ 付きの変数について、もう一つ注意したい性質があります。これらの値はビルド時に JavaScript の中へインライン展開されるという点です。ビルドした瞬間の値がコードに焼き込まれるため、あとから .env の値を書き換えても、再ビルドしない限りブラウザ側の値は変わりません。本番環境で NEXT_PUBLIC_ の値を変更したいときは、環境変数を更新したうえで必ずアプリケーションをビルドし直す必要があります。逆に、プレフィックス無しのサーバー側変数は実行時に読まれるため、この制約はありません。
値が反映されない・undefined になるとき
環境変数まわりでよくあるつまずきは、原因がいくつかに絞られます。症状別に見ていきます。
クライアント側で undefined になる
クライアントコンポーネントで process.env.SOMETHING が undefined になる場合、ほとんどは NEXT_PUBLIC_ プレフィックスの付け忘れです。前述のとおり、プレフィックスの無い変数はブラウザ向けのバンドルに含まれません。ブラウザから参照したい値なら、変数名を NEXT_PUBLIC_ で始まるように付け替え、コード側の参照も合わせて直します。なお、秘匿値をブラウザで使いたいという状況そのものが設計を見直すサインなので、まず「本当にクライアントで必要な値か」を考えるのがおすすめです。
変更が反映されない
.env.local を編集したのに値が変わらないときは、開発サーバーの再起動を忘れているケースが大半です。env ファイルは開発サーバーの起動時に読み込まれるため、起動中に書き換えても自動では反映されません。next dev を一度止めて起動し直すと、新しい値が読まれます。また本番で NEXT_PUBLIC_ の値を変えたのに反映されないときは、前の章で触れたとおりビルド時に値が固定されるためで、再ビルドが必要です。
ファイル名や置き場所が違う
env ファイルはプロジェクトのルート(package.json と同じ階層)に置く必要があります。app/ や src/ の中に置いても読み込まれません。またファイル名を .env.local.txt のように拡張子付きにしてしまう、あるいは ENV_KEY = value のようにイコールの前後にスペースを入れてしまう、といった小さなミスも値が読めない原因になります。ファイル名・置き場所・記法を順に確認すると解決しやすいです。
まとめ
Next.js の環境変数は、.env.local に キー=値 の形で書き、process.env.キー名 で参照します。.env.local は秘匿値を含むので .gitignore で Git 管理から外します。ファイルは .env(共通)、.env.development / .env.production(環境別)、.env.local(ローカル上書き・秘匿値)の順で優先度が決まり、.env.local が最も強く効きます。最も重要なのが NEXT_PUBLIC_ プレフィックスで、これを付けない変数はサーバー側でしか参照できずブラウザに漏れないため秘匿情報向き、付けた変数はビルド時に JavaScript へ埋め込まれブラウザからも参照できる代わりに秘密は入れられません。クライアント側で undefined になるならプレフィックスの付け忘れ、値が反映されないなら開発サーバーの再起動(NEXT_PUBLIC_ なら再ビルド)を疑う、と覚えておけば大きなつまずきは避けられます。