はじめに
皆さんこんにちは!エンジニアの細川です。
皆さんは環境変数を扱うときに型付けをどのように行っていますか?
いろいろ方法はあると思いますが、今回はNestJSでt3envというパッケージを利用する方法について紹介したいと思います!
t3envはESMOnlyのパッケージになりますので、NestJSでESMOnlyパッケージを利用したい方の参考にもなれば幸いです!
注意点
今回紹介する方法はNode.JSのver.が22以上でないと利用できません。
前提
今回の各パッケージなどのバージョンは以下になります。
パッケージ | バージョン |
---|---|
Node.js | v22.9.0 |
NestJS | v10.0.0 |
t3env | 0.11.1 |
t3envとは?
t3envは環境変数を型安全に利用できるパッケージです。
皆さん、ご存知の通り通常環境変数はprocess.env
などで取得すると思います。型がstring | undefined
であり、使いにくかったり、設定し忘れていることに気づけなかったりなど皆さんも環境変数に悩まされることは多いのではないでしょうか?
t3envを利用するとzodで環境変数にvalidationをつけることができます。
また、以下のようにサジェストをしてくれたり説明を付けられたりと、ちょっとした定義をするだけで、かなり環境変数を使いやすくなります!
また、NestJSで利用する場合、環境変数が設定されていないと、以下のように環境変数がセットされていないエラーを出してくれてサーバーを起動できないため、設定し忘れを防ぐこともできます。
t3envの導入
以下のコマンドを叩いて、t3envを導入します。NextやNuxtで使う場合はそれぞれ用のパッケージを導入すれば良さそうですが、今回はNestで利用するので、coreのパッケージを導入します。
npm install @t3-oss/env-nextjs zod
pnpmの場合は以下コマンドを叩いてください。
pnpm add @t3-oss/env-nextjs zod
NextやNuxtの場合はそれぞれの導入の仕方を公式ドキュメントに記載してくれているので、そちらを参考にしてください。
定義ファイルの作成
t3envでは環境変数の定義用のファイルを作成する必要があるので、公式ドキュメントに従って、以下のようにsrc/env.ts
を作成します。
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
/**
* DB接続情報 <- このように環境変数の説明も書けます。
*/
DATABASE_URL: z.string().url(),
OPEN_AI_API_KEY: z.string().min(1),
},
/**
* The prefix that client-side variables must have. This is enforced both at
* a type-level and at runtime.
*/
clientPrefix: "PUBLIC_",
client: {
PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
},
/**
* What object holds the environment variables at runtime. This is usually
* `process.env` or `import.meta.env`.
*/
runtimeEnv: process.env,
/**
* By default, this library will feed the environment variables directly to
* the Zod validator.
*
* This means that if you have an empty string for a value that is supposed
* to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag
* it as a type mismatch violation. Additionally, if you have an empty string
* for a value that is supposed to be a string with a default value (e.g.
* `DOMAIN=` in an ".env" file), the default value will never be applied.
*
* In order to solve these issues, we recommend that all new projects
* explicitly specify this option as true.
*/
emptyStringAsUndefined: true,
});
このようにserverとclientで利用する環境変数を分けて定義できたり、zodのschemaによるvalidationも行うことができます。
より細かい定義方法は公式ドキュメントなどを参照してみてください。
こちらの記事でも分かりやすく書いてくれていました。
NestJSでESMOnlyパッケージを利用する方法
通常ではこれだけで利用することができるのですが、実はt3envはESMOnlyのパッケージであり、commonJSを採用しているNestJSではそのままでは利用することができません。NestJSをESM化する方法などもありますが、既存のコードがある状態だと変更に不安があるうえに、変更量も多くなってしまい、大変です。そこで、今回はNode.jsの--experimental-require-module
オプションを利用します。こちらのオプションはNode.jsのver.22でリリースされたものののようですので、22以下の方は利用できないかもしれません(参考)。
package.jsonのNestの起動コマンドに--experimental-require-module
オプションを追加します。
// package.json
"build": "NODE_OPTIONS='--experimental-require-module' nest build",
"start": "NODE_OPTIONS='--experimental-require-module' nest start",
"start:dev": "NODE_OPTIONS='--experimental-require-module' nest start --watch",
"start:debug": "NODE_OPTIONS='--experimental-require-module' NODE_ENV=development DEBUG=true nest start --debug --watch",
"start:prod": "NODE_OPTIONS='--experimental-require-module' node dist/main",
...その他のコマンド
これを追記するとNestJSのサーバーが立ち上げられるようになります。
他のESMOnlyのパッケージを導入する際もこの手順で使えるようになるはずです。
moduleResolutionエラーの対処
サーバー自体は先ほどのオプションを追加すれば立ち上がるようになるのですが、src/env.ts
のファイルを見ると以下のようなエラーが出ていると思います。
エラーをそのまま読むと、moduleResolution
という項目をnode16
もしくは、nodenext
、もしくはbundler
にすればいいとのことですが、この設定にするためにはtsconfig.jsonのmodule
もcommonjs
ではなくnodenext
などにする必要があります。
moduleResolution
はパス解決のための設定のようなので(参考)、moduleResolution
を設定するのではなく、t3envのパスを直接解決してあげることでこの問題に対処します。
具体的には、tsconfig.jsonに以下の記述を追記します。
{
"compilerOptions": {
"module": "commonjs",
// 他の記述
"paths": {
"@t3-oss/env-core": [
"../../node_modules/@t3-oss/env-core/dist/index.d.ts" // t3envのパッケージの実体へのパス
]
}
},
"include": [
"src/**/*.ts",
"../../node_modules/@t3-oss/env-core/dist/index.d.ts" // <- こちらも追記
]
}
念のため全文も掲載しておきます。
もし追記して動作しないようでしたら、他の設定項目も確認してみてください。
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"paths": {
"@t3-oss/env-core": [
"../../node_modules/@t3-oss/env-core/dist/index.d.ts"
]
}
},
"include": [
"src/**/*.ts",
"../../node_modules/@t3-oss/env-core/dist/index.d.ts"
]
}
利用方法
以上で準備完了です。
利用する際は、まず通常と同じ通り、.env
などに実際の値を記載しておきます。
そして以下のようにsrc/env.ts
からimportを記述することで利用することができます。
import { env } from "./env";
const dbUrl = env.DATABASE_URL
型やサジェストも正しく機能していることが確認できるかと思います。
テストについて
今回の方法を試したところ、僕の環境ではJestでtestが動きませんでした。ちょうどvitestに切り替えるタイミングでしたので、Jestで動くようにする対応は行いませんでした。
vitestでは特に設定しなくてもESMも扱えるので、vitestでは問題なく動作しました。もしJestで動作しない方はvitestの導入も検討してみてください。NestJSでVitestを導入する際はこちらの記事を参考にさせていただきました。
まとめ
-
t3envを利用することで、環境変数をサジェストしてくれたり、型安全に利用できたり、定義し忘れを防止することができる!
-
NestJSでESMOnlyパッケージを利用する際には、
--experimental-require-module
オプションを利用する"build": "NODE_OPTIONS='--experimental-require-module' nest build", "start": "NODE_OPTIONS='--experimental-require-module' nest start", "start:dev": "NODE_OPTIONS='--experimental-require-module' nest start --watch", "start:debug": "NODE_OPTIONS='--experimental-require-module' NODE_ENV=development DEBUG=true nest start --debug --watch", "start:prod": "NODE_OPTIONS='--experimental-require-module' node dist/main",
-
t3envの型定義のパス解決のために、tsconfig.jsonに以下を追記する。
{ "compilerOptions": { "module": "commonjs", // 他の記述 "paths": { "@t3-oss/env-core": [ "../../node_modules/@t3-oss/env-core/dist/index.d.ts" // t3envのパッケージの実体へのパス ] } }, "include": [ "src/**/*.ts", "../../node_modules/@t3-oss/env-core/dist/index.d.ts" // <- こちらも追記 ]
-
testはvitestだと楽!
おわりに
今回はNestJSで環境変数を型安全に利用する手段としてt3envを採用しました。--experimental-require-module
オプションのおかげで、NestJSでもESMOnlyのパッケージでも導入しやすくなったと思うので、ぜひ皆さんもt3envや他のESMパッケージの利用を検討してみてください。
他にもTypeScriptのあれこれを記事にしているので、良かったら読んでみてください!
参考にさせていただいた記事
https://zenn.dev/ptna/articles/28b20f303a3cfb
https://zenn.dev/hayato94087/articles/3e4128feddffb9