VercelでAPIキーを安全に扱う方法
Next.js と Vercel でAPIキーを漏らさないための手順です。NEXT_PUBLIC_ の落とし穴、サーバー専用変数の読み方、Production / Preview / Development の分け方、vercel env の使い方、鍵が漏れたときの対処まで扱います。
執筆:住所APIナビ編集部
住所APIの中には、Google や Yahoo! のようにAPIキーが必要なものがあります。キーの流出は「うっかり」で起きるのではなく、フレームワークの仕様を知らないまま書くと自動的に起きます。ここでは Next.js と Vercel の組み合わせで、どこにキーを置き、どこから読み、漏れたら何をするかを整理します。
動作環境
以下の組み合わせを前提に書いています。掲載しているコードは構文チェックを自動で行っていますが、 断片であるため完全な型チェックは通していません。ご自身の環境で確認してください。
- Node.js 24.x
- Next.js 16(App Router)
- TypeScript 5.9(strict)
- Vercel CLI(
vercel envサブコマンドが使えるバージョン)
Vercel のドキュメントには、プレビュー環境向けの環境変数機能は Vercel CLI 22.0.0 以降、ローカル開発向けの機能は 21.0.1 以降が必要と記載されています。古い CLI を使っている場合は先に更新してください。
1. NEXT_PUBLIC_ を付けた瞬間にブラウザへ出る
これが最も多い事故です。
Next.js のドキュメントは、NEXT_PUBLIC_ を付けた変数について「ビルド時に値をJSバンドルへインライン展開し、process.env.[variable] への参照をハードコードされた値に置き換える」と説明しています。つまり NEXT_PUBLIC_ を付けた時点で、その値はブラウザに配信されるJavaScriptの中に文字列として書き込まれます。
たとえば .env.local に ADDRESS_API_KEY ではなく NEXT_PUBLIC_ADDRESS_API_KEY という名前で鍵を書いたとします。それだけで、ビルド後の .next/static/chunks/*.js にキーの文字列がそのまま入ります。DevTools を開けば誰でも読めます。コードを1行も変えていなくても、変数名の接頭辞だけで公開されます。
このサイトのリポジトリでは、NEXT_PUBLIC_ と KEY / SECRET / TOKEN / PASSWORD を組み合わせた変数名がドキュメントのコード例に混ざっていないかをテストで検査しています。同じ検査は自分のリポジトリにも置けます。
ビルド成果物に混ざっていないかは、機械的に確認できます。
# キーの先頭数文字(例: sk_live_)がクライアントバンドルに含まれていないか確認する。
pnpm build
grep -rl "sk_live_" .next/static || echo "クライアントバンドルには含まれていません"CI に入れておくと、うっかり NEXT_PUBLIC_ を付けたPRでビルドを落とせます。
**判断基準はひとつです。ブラウザに見えて困る値には NEXT_PUBLIC_ を付けない。**サイトのURLや公開用のプロジェクトIDのように、見えても困らない値だけが対象です。
2. サーバー専用の環境変数として読む
Next.js のドキュメントは「既定では環境変数はサーバー上でのみ利用可能」と述べています。NEXT_PUBLIC_ を付けなければ、その値は Node.js 側にしか存在しません。読む場所は Route Handler か Server Component に限定します。
// src/app/api/address-validate/route.ts
import { NextResponse } from 'next/server'
// このファイルはサーバーでのみ実行される。ここで読む限りブラウザには渡らない。
const API_KEY = process.env.ADDRESS_API_KEY
export async function POST(request: Request) {
// 起動時ではなくリクエスト時に検査する。値が無いまま500を返し続けるより、
// 何が足りないかを明示したほうが運用で早く気付ける。
if (!API_KEY) {
console.error('ADDRESS_API_KEY is not set')
return NextResponse.json({ error: 'server_misconfigured' }, { status: 500 })
}
const body = (await request.json()) as { address?: string }
const address = body.address?.trim()
if (!address) {
return NextResponse.json({ error: 'invalid_request' }, { status: 400 })
}
try {
const upstream = await fetch('https://api.example.com/v1/validate', {
method: 'POST',
headers: {
// キーはヘッダーで渡す。クエリ文字列に入れるとアクセスログに残る。
Authorization: `Bearer ${API_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ address }),
signal: AbortSignal.timeout(5000),
cache: 'no-store',
})
if (upstream.status === 429) {
return NextResponse.json({ error: 'rate_limited' }, { status: 429 })
}
if (!upstream.ok) {
// 上流の本文をそのまま返さない。キーやプラン情報が混ざることがある。
console.error('upstream error', upstream.status)
return NextResponse.json({ error: 'upstream_error' }, { status: 502 })
}
const data = (await upstream.json()) as { normalized?: string }
return NextResponse.json({ normalized: data.normalized ?? null })
} catch (error) {
const timedOut = error instanceof Error && error.name === 'TimeoutError'
// エラーメッセージに住所やキーを載せない(住所は個人情報)。
console.error('address validation failed', timedOut ? 'timeout' : 'network')
return NextResponse.json(
{ error: timedOut ? 'timeout' : 'network_error' },
{ status: 504 },
)
}
}押さえておきたい点が3つあります。
- エラー本文をそのまま転送しない。 上流のエラーメッセージにはキーの一部やアカウント情報が含まれることがあります。自分で定義したエラーコードに置き換えて返します。
- ログに何を書くか決める。 キーはもちろん、住所も個人情報です。「どの処理が、どの理由で失敗したか」までに絞ります。
- 静的レンダリングとの関係に注意する。 Server Component が静的にレンダリングされる場合、環境変数はビルド時に評価されます。リクエスト時の値を使いたい場合、Next.js のドキュメントは
next/serverのconnection()やcookies()/headers()を使って動的レンダリングに切り替える方法を示しています。
サーバー専用モジュールがクライアントへ紛れ込むのを機械的に防ぎたい場合は、server-only パッケージを先頭で import しておくと、クライアントコンポーネントから読み込まれた時点でビルドが失敗します。
3. 環境ごとに変数を分ける
Vercel の環境変数は、適用先の環境を選んで登録します。
| 環境 | 適用先 |
|---|---|
| Production | 本番デプロイ。本番ブランチへのpush、または vercel --prod |
| Preview | 本番ブランチ以外へのpushで作られるデプロイ |
| Development | ローカル開発。vercel dev や vercel env pull で取得 |
**Production と Preview で同じキーを使わないでください。**Preview デプロイのURLは共有されやすく、そこから本番のキーが使われると、本番の利用枠を消費します。Preview 用に別のキー(できれば利用枠の小さいもの)を発行するのが安全です。
Vercel のドキュメントによれば、Preview の変数は「本番以外の全ブランチ」に適用するか、特定のブランチだけに適用するかを選べます。ブランチ指定の変数は、同じ名前の他の Preview 変数を上書きします。
コマンドで登録する場合は次のとおりです。
# プロジェクトをローカルにリンクする(.vercel/ が作られる)
vercel link
# 環境を指定して追加する。値は対話的に入力する。
vercel env add ADDRESS_API_KEY production
vercel env add ADDRESS_API_KEY preview
# 一覧(値は表示されないことがある)
vercel env ls
# 更新・削除
vercel env update ADDRESS_API_KEY production
vercel env rm ADDRESS_API_KEY previewローカルで使う値は vercel env pull で取得します。
# 書き出し先のファイルを明示的に指定する(既定のファイル名に依存しない)
vercel env pull .env.local
# Preview の値を取得したい場合
vercel env pull .env.local --environment=preview
# 特定ブランチの Preview 変数
vercel env pull .env.local --environment=preview --git-branch=feature-branchファイルに書き出さずに実行したい場合は vercel env run が使えます。ディスクに秘密を置かずに済むので、共有マシンではこちらが向いています。
vercel env run -- next dev
vercel env run -e preview -- pnpm testダッシュボードや vercel env add で値を変えたあとは、vercel env pull をやり直さないとローカルは古いままです。「ローカルだけ動かない」の原因の多くはこれです。
4. .env.local を絶対にコミットしない
Next.js のドキュメントも「create-next-app の既定テンプレートはすべての .env ファイルを .gitignore に入れる。これらをリポジトリにコミットしたい場面はまずない」と警告しています。
.gitignore は次のように書きます。
# .gitignore
# env files — 実際の値は絶対にコミットしない
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel link が作るディレクトリ
.vercelvercel link を実行すると .gitignore に .env* が追記されることがあります。その場合、**ドキュメントとして配りたい .env.example まで無視されます。**次の1行を後ろに足して除外を打ち消してください。
.env*
!.env.example.env.example にはキーの名前と説明だけを書き、値は書きません。
# .env.example
# 住所検証APIのキー。サーバー専用。NEXT_PUBLIC_ を付けてはいけない。
# 取得先: 各プロバイダの管理画面
ADDRESS_API_KEY=
# 公開してよい値だけが NEXT_PUBLIC_ を名乗れる
NEXT_PUBLIC_SITE_URL=https://example.jp無視されているかは推測せず、コマンドで確かめます。
# 無視されていれば、該当する .gitignore の行番号が表示される
git check-ignore -v .env.local
# 追跡されていないことの確認(何も出なければ安全)
git ls-files | grep -E '^\.env' | grep -v '\.env\.example'すでにコミットしてしまった場合は、git rm --cached .env.local で追跡から外したうえで、**次のセクションの手順に進んでください。**追跡を外しただけでは、履歴にも、他人のクローンにも値は残ります。
5. 鍵が漏れたときの手順
順番が重要です。焦って履歴の削除から始めるのが、いちばんまずい対応です。
- 失効させる(最優先)。 プロバイダの管理画面で、漏れたキーを無効化します。ここが終わるまで、他の作業には価値がありません。
- 新しいキーを発行して差し替える。
vercel env updateで登録し直し、再デプロイします。ローカルはvercel env pullをやり直します。 - 影響範囲を確認する。 プロバイダ側の利用ログと請求額を見て、想定外の利用がないか確かめます。
- 公開済みのビルド成果物を確認する。
NEXT_PUBLIC_で漏れていた場合、過去のデプロイURLからバンドルを取得できます。不要な Preview デプロイは削除するか、アクセスを制限します。 - 履歴からの削除は最後。
git filter-repoなどで履歴を書き換えられますが、**履歴を消しても漏洩そのものは取り消せません。**フォークやクローン、CIのキャッシュ、コード検索サービスのインデックスに残っている可能性があります。すでに失効させたキーであれば、履歴の書き換えは「掃除」であって「対処」ではありません。
# 差し替えの流れ(失効は管理画面で先に済ませておく)
vercel env update ADDRESS_API_KEY production
vercel env update ADDRESS_API_KEY preview
vercel env pull .env.localチーム開発では、共有チャットやIssueにキーを貼らないルールを先に決めておくのが効きます。貼られてしまったキーは、投稿を削除しても失効させる必要があります。
6. クライアントから外部APIを直接呼ばない
キーが不要なAPIであっても、Route Handler をはさむ構成をおすすめします。理由は3つです。
- キーを隠せる。 将来キーが必要なAPIに乗り換えても、フロントエンドのコードは変わりません。
- レート制限を自前で掛けられる。 上流に投げる前に、IPやセッション単位で回数を絞れます。上流の制限に当たってサービス全体が止まるのを防げます。
- 差し替えが1ファイルで済む。 上流APIが停止・仕様変更・値上げになったとき、直すのは Route Handler だけです。クライアントから直接呼んでいると、レスポンス形式の違いがコンポーネント全体に波及します。
副次的な効果として、CORS の可否に依存しなくなること、上流のレスポンスをキャッシュできること(キャッシュ可否は各APIの規約を確認してください)、そして上流のエラーを自分のエラーコードに正規化できることがあります。
実装の例は Next.jsで郵便番号から住所を自動入力する方法 の Route Handler が参考になります。
7. このサイト自体はAPIキーを使っていません
参考までに、住所APIナビの MVP はAPIキーを一切使っていません。
- 第三者の住所APIをプロキシしていないため、サーバー側に外部APIの鍵を持つ必要がありません。
- 日本住所表記ゆれチェッカー は入力をブラウザ内だけで処理し、サーバーへ送信しません。
- 環境変数として持っているのは、正規URLの組み立てに使う
NEXT_PUBLIC_SITE_URLだけです。これは公開情報なのでNEXT_PUBLIC_で問題ありません。
将来プロバイダのデモを載せる場合も、キーはサーバー専用の変数として扱い、NEXT_PUBLIC_ は付けない方針です。編集方針はこのサイトの調査方法に記載しています。
関連ページ
- Next.jsで郵便番号から住所を自動入力する方法 — Route Handler をはさむ実装
- 日本の郵便番号検索API比較 — キーが必要なAPI・不要なAPIの一覧
- zipcloud 郵便番号検索API — キー不要で試せるAPIの例
出典
- Vercel: Environment variables(新しいタブで開く)確認日 2026-07-29
- Vercel CLI: vercel env(新しいタブで開く)確認日 2026-07-29
- Next.js: How to use environment variables in Next.js(新しいタブで開く)確認日 2026-07-29
- Next.js: Server and Client Components(server-only による環境汚染の防止)(新しいタブで開く)確認日 2026-07-29