T3 Envで環境変数をタイプセーフに扱う
はじめに
- 環境変数を扱うときに、型が
string | undefined
になり困ったり、環境変数名を打ち間違えたり、型が異なっていたりすることがあります。 - T3 Env[1] を利用し、Next.js で環境変数のタイプセーフに扱う方法を紹介します。
作業コードはこちらにあります。
T3 Env
Next.js の App Router でタイプセーフに環境変数を扱う場合は、T3-env がおすすめです。
T3 Env は以下の特徴があります。
サジェスト
サジェストで環境変数を表示してくれます。
タイプセーフ
環境変数に型付けし、TypeScript でタイプセーフに環境変数を扱えます。
process.env を使う場合、型が string | undefined
になるため、環境変数の値を利用する際に、undefined
のチェックを行う必要があります。これを回避できます。
環境変数の説明
環境変数の説明を追加できます。
ビルド時の型検証
T3-env はビルド時に型の検証し、エラーを出力します。型の検証は Zod[2]を利用しており、Zod の範囲であれば型の検証が可能です。
- 環境変数が定義されていない(例:
DEBUG_MESSAGE
) - 環境変数の型・制約が異なる(例:
DATABASE_URL
、DEBUG_EMAIL
、DEBUG_EMOJI
)
以下がビルドエラーの例です。
$ pnpm build
- info Loaded env from /Users/hayato94087/Private/next-t3env-sample/.env
❌ Invalid environment variables: {
DATABASE_URL: [ 'Invalid url' ],
DEBUG_EMAIL: [ 'Invalid email' ],
DEBUG_EMOJI: [ 'Invalid emoji' ],
DEBUG_MESSAGE: [ 'Required' ]
型の検証はもちろんですが、タイポなどもこれで回避できます。
サイト
以下がライブラリーサイトです。
紹介されている動画
新規にプロジェクトを作成
作業するプロジェクトを新規に作成していきます。
長いので、折りたたんでおきます。
新規プロジェクト作成と初期環境構築の手順詳細
$ pnpm create next-app@latest next-t3env-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd next-t3env-sample
以下の通り不要な設定を削除し、プロジェクトの初期環境を構築します。
@tailwind base;
@tailwind components;
@tailwind utilities;
export default function Home() {
return (
<main className="text-lg">
テストページ
</main>
)
}
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="">{children}</body>
</html>
);
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
plugins: [],
};
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
+ "baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
+ "@app/*": ["./src/app/*"],
+ "@components/*": ["./src/components/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
コミットします。
$ pnpm build
$ git add .
$ git commit -m "新規にプロジェクトを作成し, 作業環境を構築"
T3-envのパッケージをインストール
$ pnpm add @t3-oss/env-nextjs zod
スキーマを作成
スキーマの作成方法は 2 種類あります。ここでは、サーバサイドとクライアントの環境変数を同一ファイルに記述する方法を紹介します。
$ touch src/env.mjs
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
// サーバサイドの環境変数
server: {
/** データベースの接続先のURL */
DATABASE_URL: z.string().url(),
/** OPEN AI の API KEY */
API_KEY: z.string().min(1),
/** メールアドレス */
DEBUG_EMAIL: z.string().email(),
/** デバッグ用の絵文字 */
DEBUG_EMOJI: z.string().emoji(),
/** デバッグ用のメッセージ */
DEBUG_MESSAGE: z.string().min(5),
},
// クライアントサイドの環境変数
client: {
// クライアントサイドで利用するキー
NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
/** デバッグ用のメッセージ */
NEXT_PUBLIC_DEBUG_MESSAGE: z.string().min(5),
},
// If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually
// runtimeEnv: {
// DATABASE_URL: process.env.DATABASE_URL,
// API_KEY: process.env.API_KEY,
// NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
// },
// For Next.js >= 13.4.4, you only need to destructure client variables:
experimental__runtimeEnv: {
NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
NEXT_PUBLIC_DEBUG_MESSAGE: process.env.NEXT_PUBLIC_DEBUG_MESSAGE,
},
});
スキーマのポイントを説明します。
ポイント1:サーバーサイドで利用する環境変数
server
はサーバーサイドで利用する環境変数を定義します。
...
export const env = createEnv({
// サーバサイドの環境変数
server: {
/** データベースの接続先のURL */
DATABASE_URL: z.string().url(),
/** OPEN AI の API KEY */
API_KEY: z.string().min(1),
/** メールアドレス */
DEBUG_EMAIL: z.string().email(),
/** デバッグ用の絵文字 */
DEBUG_EMOJI: z.string().emoji(),
/** デバッグ用のメッセージ */
DEBUG_MESSAGE: z.string().min(5),
},
...
});
ポイント2:クライアントで利用する環境変数
client
はクライアントで利用する環境変数を定義します。NEXT_PUBLIC_PUBLISHABLE_KEY
と NEXT_PUBLIC_DEBUG_MESSAGE
はクライアントで利用を想定した環境変数です。
...
export const env = createEnv({
...
// クライアントサイドの環境変数
client: {
// クライアントサイドで利用するキー
NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
/** デバッグ用のメッセージ */
NEXT_PUBLIC_DEBUG_MESSAGE: z.string().min(5),
},
...
// For Next.js >= 13.4.4, you only need to destructure client variables:
experimental__runtimeEnv: {
NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
NEXT_PUBLIC_DEBUG_MESSAGE: process.env.NEXT_PUBLIC_DEBUG_MESSAGE,
},
});
ポイント3:zodを利用した型定義
zod を利用し環境変数の型を定義します。
-
DATABASE_URL
は URL 形式 -
API_KEY
は 1 文字以上の文字列 -
DEBUG_EMAIL
はメールアドレス形式 -
DEBUG_EMOJI
は絵文字を含む文字列 -
DEBUG_MESSAGE
は 5 文字以上の文字列 -
NEXT_PUBLIC_PUBLISHABLE_KEY
は 1 文字以上の文字列 -
NEXT_PUBLIC_DEBUG_MESSAGE
は 5 文字以上の文字列
...
export const env = createEnv({
// サーバサイドの環境変数
server: {
/** データベースの接続先のURL */
DATABASE_URL: z.string().url(),
/** OPEN AI の API KEY */
API_KEY: z.string().min(1),
/** メールアドレス */
DEBUG_EMAIL: z.string().email(),
/** デバッグ用の絵文字 */
DEBUG_EMOJI: z.string().emoji(),
/** デバッグ用のメッセージ */
DEBUG_MESSAGE: z.string().min(5),
},
// クライアントサイドの環境変数
client: {
// クライアントサイドで利用するキー
NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
/** デバッグ用のメッセージ */
NEXT_PUBLIC_DEBUG_MESSAGE: z.string().min(5),
},
...
});
ポイント4:環境変数の説明を追加
環境変数の説明をコメントを追記できます。実際のエディターでの資料例は以下の画像です。
設定は以下のようになります。
...
export const env = createEnv({
// サーバサイドの環境変数
server: {
/** データベースの接続先のURL */
DATABASE_URL: z.string().url(),
/** OPEN AI の API KEY */
API_KEY: z.string().min(1),
/** メールアドレス */
DEBUG_EMAIL: z.string().email(),
/** デバッグ用の絵文字 */
DEBUG_EMOJI: z.string().emoji(),
/** デバッグ用のメッセージ */
DEBUG_MESSAGE: z.string().min(5),
},
// クライアントサイドの環境変数
client: {
// クライアントサイドで利用するキー
NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
/** デバッグ用のメッセージ */
NEXT_PUBLIC_DEBUG_MESSAGE: z.string().min(5),
},
...
});
以下が実際の環境変数の説明一覧です。
環境変数 | 説明 |
---|---|
DATABASE_URL | データベースの接続先のURL |
API_KEY | OPEN AI の API KEY |
DEBUG_EMAIL | メールアドレス |
DEBUG_EMOJI | デバッグ用の絵文字 |
DEBUG_MESSAGE | デバッグ用のメッセージ |
NEXT_PUBLIC_PUBLISHABLE_KEY | クライアントサイドで利用するキー |
NEXT_PUBLIC_DEBUG_MESSAGE | デバッグ用のメッセージ |
ポイント5:環境変数の設定ファイルの読み込み
詳細は後ほど説明しますが、コード上で環境変数を取り扱う場合は、env.mjs
を import
し、env
からアクセスします。
import { env } from "@/env.mjs";
export default function Home() {
const debugMessage = env.DEBUG_MESSAGE;
return (
<main className="text-lg">
<div>{debugMessage}</div>
</main>
);
}
これにより、サジェストで環境変数を表示してくれます。
また、環境変数に型付けし、TypeScript でタイプセーフに環境変数を扱えます。as string
とかしなくてすみます。
ポインㇳ6:サーバサイドとクライアントの環境変数を同時に記述
クライアントとサーバサイドで利用する環境変数の設定を 1 つのファイルで対応できます。良い点としてサーバサイドとクライアントを意識して環境変数の設定ファイル(env.mjs
)の読み込み先を変更する必要がありません。
一方で、同一ファイルに記述するデメリットがあります。クライアントにサーバサイドで利用している環境変数の名前が漏れてしまうことです。サーバサイドの環境が可視化されるのはセキュリティ的には望ましくないので、次に紹介するスキーマを分割して記述することをおすすめします。
スキーマを分割
サーバーサイドとクライアントの環境変数ファイルを分割します。
$ mkdir -p src/env/
$ touch src/env/client.mjs
$ touch src/env/server.mjs
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
// サーバサイドの環境変数
server: {
/** データベースの接続先のURL */
DATABASE_URL: z.string().url(),
/** OPEN AI の API KEY */
API_KEY: z.string().min(1),
/** メールアドレス */
DEBUG_EMAIL: z.string().email(),
/** デバッグ用の絵文字 */
DEBUG_EMOJI: z.string().emoji(),
/** デバッグ用のメッセージ */
DEBUG_MESSAGE: z.string().min(5),
},
experimental__runtimeEnv: {},
});
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
// クライアントサイドの環境変数
client: {
// クライアントサイドで利用するキー
NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
/** デバッグ用のメッセージ */
NEXT_PUBLIC_DEBUG_MESSAGE: z.string().min(5),
},
experimental__runtimeEnv: {
NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
NEXT_PUBLIC_DEBUG_MESSAGE: process.env.NEXT_PUBLIC_DEBUG_MESSAGE,
},
});
コミットします。
コミット処理
$ pnpm build
$ git add .
$ git commit -m "T3-env のパッケージを追加しスキーマを作成"
スキーマは作成しました。が、この時点では、next.config.js
にて作成したスキーマを利用する設定をしていないため、ビルド時にスキーマ検証は行われません。よって、環境変数のファイル(env.local
)がなくても、エラーになりません。
ビルド時にスキーマ検証できるよう設定
ビルド時に T3-env でスキーマをベースに環境変数を検証できるようにします。next.config.js
は import
に対応していないため、next.config.mjs
に変更します。
$ mv next.config.js next.config.mjs
先程のスキーマを import
します。
// 同一ファイルを利用する場合
// import "./src/env.mjs";
// ファイルを分割する場合
import "./src/env/client.mjs";
import "./src/env/server.mjs";
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
ビルドしてみる
では、実際にビルドして、環境変数の型検証ができるか確認してみます。なお、現在は、.env.local
が存在しない状態です。
$ ls .env*
zsh: no matches found: .env*
ビルドすると、NEXT_PUBLIC_PUBLISHABLE_KEY
と DEBUG_CLIENT_MESSAGE
が定義されていないとエラーが出ます。
$ pnpm build
❌ Invalid environment variables: {
NEXT_PUBLIC_PUBLISHABLE_KEY: [ 'Required' ],
NEXT_PUBLIC_DEBUG_MESSAGE: [ 'Required' ]
}
エラーが出た環境変数を定義します。
$ touch .env.local
NEXT_PUBLIC_PUBLISHABLE_KEY=oNSApA4gVsqVD05EHSYP
NEXT_PUBLIC_DEBUG_MESSAGE="HELLO CLIENT"
再度、ビルドすると、今度はサーバサイドの環境変数が定義されていないというエラーが返ります。
$ pnpm build
❌ Invalid environment variables: {
DATABASE_URL: [ 'Required' ],
API_KEY: [ 'Required' ],
DEBUG_EMAIL: [ 'Required' ],
DEBUG_EMOJI: [ 'Required' ],
DEBUG_MESSAGE: [ 'Required' ]
}
zod を利用し環境変数の型を定義されています。
-
DATABASE_URL
は URL 形式 -
API_KEY
は 1 文字以上の文字列 -
DEBUG_EMAIL
はメールアドレス形式 -
DEBUG_EMOJI
は絵文字を含む文字列 -
DEBUG_MESSAGE
は 5 文字以上の文字列 -
NEXT_PUBLIC_PUBLISHABLE_KEY
は 1 文字以上の文字列 -
NEXT_PUBLIC_DEBUG_MESSAGE
は 5 文字以上の文字列
わざと、誤った値を入れていきます。
NEXT_PUBLIC_PUBLISHABLE_KEY=oNSApA4gVsqVD05EHSYP
DEBUG_CLIENT_MESSAGE="HELLO CLIENT"
+DATABASE_URL=OYrQqOcndG36oKll3xrb
+API_KEY=""
+DEBUG_EMAIL="aa"
+DEBUG_EMOJI="hogehoge"
+DEBUG_MESSAGE="a"
ビルドします。以下の通りエラーが出ます。
-
DATABASE_URL
は URL 形式ではない -
API_KEY
は 1 文字以上の文字列ではない -
DEBUG_EMAIL
はメールアドレス形式ではない -
DEBUG_EMOJI
は絵文字を含む文字列ではない -
DEBUG_MESSAGE
は 5 文字以上の文字列ではない
$ pnpm build
❌ Invalid environment variables: {
DATABASE_URL: [ 'Invalid url' ],
API_KEY: [ 'String must contain at least 1 character(s)' ],
DEBUG_EMAIL: [ 'Invalid email' ],
DEBUG_EMOJI: [ 'Invalid emoji' ],
DEBUG_MESSAGE: [ 'String must contain at least 5 character(s)' ]
では、正しい情報を入力します。
NEXT_PUBLIC_PUBLISHABLE_KEY=oNSApA4gVsqVD05EHSYP
NEXT_PUBLIC_DEBUG_MESSAGE="HELLO CLIENT"
-DATABASE_URL=OYrQqOcndG36oKll3xrb
-API_KEY=""
-DEBUG_EMAIL="aa"
-DEBUG_EMOJI="hogehoge"
-DEBUG_MESSAGE="a"
+DATABASE_URL=https://hogehoge.com/database
+API_KEY="3KflaKDOQ0F"
+DEBUG_EMAIL="hogehoge@gmail.com"
+DEBUG_EMOJI="👍"
+DEBUG_MESSAGE="HELLO SERVER"
ビルドします。形式は正しいので、エラーは出ません。
$ pnpm build
コミットします。
コミット処理
$ pnpm build
$ git add .
$ git commit -m "next.config.mjs と T3 envを連携"
動作確認
それではそれぞれのユースケースで動作確認していきましょう。
ケース1:サーバサイドで環境変数を利用
クライアントとサーバサイドのスキーマを同一ファイルで定義しているスキーマファイル(env.mjs
)を読み込み、環境変数を env
からアクセスします。
$ mkdir -p src/app/case-1
$ touch src/app/case-1/page.tsx
// スキーマを読み込む方法は二種類
// クライアントとサーバサイドのスキーマを同一ファイルで定義しているファイルを読み込む
import { env } from "@/env.mjs";
export default function Home() {
// スキーマをを読み込むことでタイプセーフに環境変数を扱える
const databaseUrl = env.DATABASE_URL;
const apiKey = env.API_KEY;
const debugEmail = env.DEBUG_EMAIL;
const debugEmoji = env.DEBUG_EMOJI;
const debugMessage = env.DEBUG_MESSAGE;
// サーバサイドのログに出力される
// 今回はビルド時に吐き出される
console.log("\n")
console.log(`Server Log`)
console.log(`DATABASE_URL : ${databaseUrl}`);
console.log(`API_KEY : ${apiKey}`);
console.log(`DEBUG_EMAIL : ${debugEmail}`);
console.log(`DEBUG_EMOJI : ${debugEmoji}`);
console.log(`DEBUG_MESSAGE : ${debugMessage}`);
return (
<main className="text-lg">
サーバサイド処理
</main>
);
}
ビルドします。ビルドすると console.log で出力したログが吐き出されます。
$ pnpm build
- info Loaded env from /Users/hayato94087/Private/next-t3env-sample/.env.local
- info Creating an optimized production build
- info Compiled successfully
- info Linting and checking validity of types
- info Collecting page data
[ ] - info Generating static pages (0/5)
Server Log
DATABASE_URL : https://hogehoge.com/database
API_KEY : 3KflaKDOQ0F
DEBUG_EMAIL : hogehoge@gmail.com
DEBUG_EMOJI : 👍
DEBUG_MESSAGE : HELLO SERVER
[= ] - info Generating static pages (3/5)
Server Log
DATABASE_URL : https://hogehoge.com/database
API_KEY : 3KflaKDOQ0F
DEBUG_EMAIL : hogehoge@gmail.com
DEBUG_EMOJI : 👍
DEBUG_MESSAGE : HELLO SERVER
- info Generating static pages (5/5)
- info Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 141 B 77.7 kB
├ ○ /case-1 141 B 77.7 kB
└ ○ /favicon.ico 0 B 0 B
+ First Load JS shared by all 77.5 kB
├ chunks/14478101-08a82aad1ad550e2.js 50.5 kB
├ chunks/215-719eb731de1ca078.js 25.1 kB
├ chunks/main-app-2253954475def1ca.js 215 B
└ chunks/webpack-47c83bed457c1f9b.js 1.64 kB
Route (pages) Size First Load JS
─ ○ /404 182 B 75.4 kB
+ First Load JS shared by all 75.3 kB
├ chunks/framework-510ec8ffd65e1d01.js 45 kB
├ chunks/main-1b74ca39d3294f57.js 28.4 kB
├ chunks/pages/_app-3e277c1f911fda65.js 195 B
└ chunks/webpack-47c83bed457c1f9b.js 1.64 kB
○ (Static) automatically rendered as static HTML (uses no initial props)
実行します。
$ pnpm start
ページを表示するとファイルがダウンロードされます。今回は、サーバ側で全ての寄りが終わっているため、クライアント側にはサーバサイドの環境変数が漏洩することはありません。
ケース2:クライアントで環境変数を利用
クライアントとサーバサイドのスキーマを同一ファイルで定義しているスキーマファイル(env.mjs
)を読み込み、環境変数を env
からアクセスします。
$ mkdir -p src/app/case-2
$ touch src/app/case-2/page.tsx
"use client"
// クライアントとサーバサイドのスキーマを同一ファイルで定義しているファイルを読み込む
import { env } from "@/env.mjs";
export default function Home() {
// スキーマをを読み込むことでタイプセーフに環境変数を扱える
const nextPublicDebugMessage = env.NEXT_PUBLIC_DEBUG_MESSAGE;
const nextPublicPublishableKey = env.NEXT_PUBLIC_PUBLISHABLE_KEY;
// クライアントのログに出力される
console.log("\n")
console.log(`Client Log`)
console.log(`NEXT_PUBLIC_DEBUG_MESSAGE : ${nextPublicDebugMessage}`);
console.log(`NEXT_PUBLIC_PUBLISHABLE_KEY : ${nextPublicPublishableKey}`);
return (
<main className="text-lg">
クライアント
</main>
);
}
ビルドし、実行します。
$ pnpm build
$ pnpm start
ページを表示するとファイルがダウンロードされます。
今回は、ファイルを開くと、スキーマがダウンロードされており、サーバサイドの環境変数の一覧が見えます。
ちなみに use client
を利用しているので console.log はクライアント側のログに出力されます。
ケース3:クライアントで環境変数を利用(スキーマを分ける)
今度はサーバサイドとクライアントのスキーマを分けます。
$ mkdir -p src/app/case-3
$ touch src/app/case-3/page.tsx
"use client"
// クライアントとサーバサイドのスキーマを同一ファイルで定義しているファイルを読み込む
import { env } from "@/env/client.mjs";
export default function Home() {
// スキーマをを読み込むことでタイプセーフに環境変数を扱える
const nextPublicDebugMessage = env.NEXT_PUBLIC_DEBUG_MESSAGE;
const nextPublicPublishableKey = env.NEXT_PUBLIC_PUBLISHABLE_KEY;
// クライアントのログに出力される
console.log("\n")
console.log(`Client Log`)
console.log(`NEXT_PUBLIC_DEBUG_MESSAGE : ${nextPublicDebugMessage}`);
console.log(`NEXT_PUBLIC_PUBLISHABLE_KEY : ${nextPublicPublishableKey}`);
return (
<main className="text-lg">
クライアント
</main>
);
}
ビルドし、実行します。
$ pnpm build
$ pnpm start
今回もスキーマはダウンロードされていますが、サーバーサイドのスキーマはダウンロードされていません。
コミットします。
コミット処理
$ pnpm build
$ git add .
$ git commit -m "created pages"
さいごに
- T3-env[1:1] を利用し、Next.js で環境変数のタイプセーフに扱う方法を紹介しました。
- スキーマファイルを同一ファイルにすることで開発の利便性は向上しますが、不用意にサーバ側の環境変数がクライアントに見えてしまいます。どのようなインフラで構築されているか、漏洩しても問題ないかを考慮して利用してください。
Discussion