メインコンテンツへスキップ
住所APIナビ

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.localADDRESS_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/serverconnection()cookies() / headers() を使って動的レンダリングに切り替える方法を示しています。

サーバー専用モジュールがクライアントへ紛れ込むのを機械的に防ぎたい場合は、server-only パッケージを先頭で import しておくと、クライアントコンポーネントから読み込まれた時点でビルドが失敗します。

3. 環境ごとに変数を分ける

Vercel の環境変数は、適用先の環境を選んで登録します。

環境適用先
Production本番デプロイ。本番ブランチへのpush、または vercel --prod
Preview本番ブランチ以外へのpushで作られるデプロイ
Developmentローカル開発。vercel devvercel 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 が作るディレクトリ
.vercel

vercel 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. 鍵が漏れたときの手順

順番が重要です。焦って履歴の削除から始めるのが、いちばんまずい対応です。

  1. 失効させる(最優先)。 プロバイダの管理画面で、漏れたキーを無効化します。ここが終わるまで、他の作業には価値がありません。
  2. 新しいキーを発行して差し替える。 vercel env update で登録し直し、再デプロイします。ローカルは vercel env pull をやり直します。
  3. 影響範囲を確認する。 プロバイダ側の利用ログと請求額を見て、想定外の利用がないか確かめます。
  4. 公開済みのビルド成果物を確認する。 NEXT_PUBLIC_ で漏れていた場合、過去のデプロイURLからバンドルを取得できます。不要な Preview デプロイは削除するか、アクセスを制限します。
  5. 履歴からの削除は最後。 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_ は付けない方針です。編集方針はこのサイトの調査方法に記載しています。

関連ページ