🌟

fastify-env+TypeBoxで環境変数を管理する

2024/01/04に公開

fastifyには環境変数を管理するfastify-envが用意されている。

今回はfastify-envを使用して環境変数から呼び出した情報をもとにDBの接続を行うプラグインを作成してみる。

GitHubにサンプルを用意したので、実際の動作は下記を参照。

使用ライブラリ

  • @fastify/env:fastifyで環境変数を管理するプラグイン。
  • @sinclair/typebox:Typescriptで静的解析を行うライブラリ。
  • dotenv:.envファイルを読み込むライブラリ。
  • cross-env:起動時に環境変数を指定するライブラリ。NODE_ENVを指定するために使用。

ディレクトリ構成

.
├── plugins
│   └── db.plugin.ts
├── postgres
│   └── Dockerfile
├── src
│   ├── infrastructure
│   │   └── config
│   │       └── env.ts
│   ├── app.ts
│   └── server.ts
├── .env.development
├── .gitignore
├── docker-compose.yml
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json

.env.development(環境変数)

環境変数の設定場所。ここではデータベースの接続先が設定されている。

DATABASE_URL=postgresql://admin:password@localhost:5432/db?schema=public

env.ts(環境変数の管理)

環境変数を読み込むための設定を行う。起動時にNODE_ENV=developmentが実行されると、.env.developmentが読み込まれる。

  • schema:環境変数を管理する。.envファイルに併せて追加する。ここでTypeboxを用いた静的解析が可能。
  • envOptions:fastify-envのオプションの設定。Usageを参考にする。
env.ts
import { TObject, Type } from '@sinclair/typebox';
import * as dotenv from 'dotenv';

declare module 'fastify' {
  interface FastifyInstance {
    config: {
      DATABASE_URL: string;
    };
  }
}

const schema: TObject = Type.Object({
  DATABASE_URL: Type.String(),
});

export const envOptions = () => {
  if (!process.env.NODE_ENV) {
    throw new Error('NODE_ENV is not defined.');
  }
  dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
  return {
    dotenv: true,
    confKey: 'config',
    schema,
    data: process.env,
  };
};

※ 環境変数を追加した際にdeclare module 'fastify'にも追加すること。

server.ts(プラグインの登録+起動)

プラグインを登録してサーバーを起動する。

  • fastifyEnv:env.tsで作成したenvOptionsを指定して、fastify-envを登録する。これによって環境変数を呼び出すことができる。
  • dbPlugin:DBの設定を行う。環境変数を使用するのでfastifyEnvの後に登録を行う。
server.ts(プラグインの登録+起動)
import fastifyEnv from '@fastify/env';
import App from './app';
import { envOptions } from './infrastructure/config/env';
import DbPlugin from '../plugins/db.plugin';

async function bootstrap() {
  const app = new App();
  await app.$server.register(fastifyEnv, envOptions());
  await app.$server.register(DbPlugin);
  await app.listen();
}

bootstrap();

db.plugin.ts(環境変数の呼び出し部分)

データベースの接続に成功した場合は接続先がログが出力され、失敗した場合はエラー内容がログに出力される。

db.plugin.ts
import { FastifyInstance, FastifyPluginOptions, FastifyPluginAsync } from 'fastify';
import { fastifyPlugin } from 'fastify-plugin';
import { Client } from 'pg';

declare module 'fastify' {
  interface FastifyInstance {
    pg: Client;
  }
}

const dbPlugin: FastifyPluginAsync = async (
  $server: FastifyInstance,
  _opts: FastifyPluginOptions,
): Promise<void> => {
  const client = new Client({
    connectionString: $server.config.DATABASE_URL,
  });

  try {
    await client.connect();
    $server.log.info(`Connected to the DB at URL: ${$server.config.DATABASE_URL}`);
  } catch (error) {
    $server.log.error(error);
    throw error
  }

  $server.addHook('onClose', async () => {
    await $server.pg.end();
  });
};

export default fastifyPlugin(dbPlugin, '4.x');

実行

DBの起動

docker-compose up postgres

サーバーの起動

npm run start:dev
実行結果(接続成功)
> fastify-env-eample@1.0.0 start:dev
> cross-env NODE_ENV=development ts-node-dev --watch -- src/server

[INFO] 18:25:14 ts-node-dev ver. 2.0.0 (using ts-node ver. 10.9.2, typescript ver. 5.3.3)
{"level":30,"time":1704360316295,"pid":19964,"hostname":"DESKTOP-UOO17L2","msg":"Connected to the DB at URL: postgresql://admin:password@localhost:5432/db?schema=public"}
{"level":30,"time":1704360316299,"pid":19964,"hostname":"DESKTOP-UOO17L2","msg":"Server listening at http://[::1]:3000"}
{"level":30,"time":1704360316300,"pid":19964,"hostname":"DESKTOP-UOO17L2","msg":"Server listening at http://127.0.0.1:3000"}
実行結果(接続失敗)
> fastify-env-eample@1.0.0 start:dev
> cross-env NODE_ENV=development ts-node-dev --watch -- src/server

[INFO] 18:33:57 ts-node-dev ver. 2.0.0 (using ts-node ver. 10.9.2, typescript ver. 5.3.3)
{"level":50,"time":1704360839127,"pid":11576,"hostname":"DESKTOP-UOO17L2","err":{"type":"AggregateError","message":"","stack":"AggregateError: \n    at internalConnectMultiple (node:net:1114:18)\n    at afterConnectMultiple (node:net:1667:5)","aggregateErrors":[{"type":"Error","message":"connect ECONNREFUSED ::1:5432","stack":"Error: connect ECONNREFUSED ::1:5432\n    at createConnectionError (node:net:1634:14)\n    at afterConnectMultiple (node:net:1664:40)","errno":-4078,"code":"ECONNREFUSED","syscall":"connect","address":"::1","port":5432},{"type":"Error","message":"connect ECONNREFUSED 127.0.0.1:5432","stack":"Error: connect ECONNREFUSED 127.0.0.1:5432\n    at createConnectionError (node:net:1634:14)\n    at afterConnectMultiple (node:net:1664:40)","errno":-4078,"code":"ECONNREFUSED","syscall":"connect","address":"127.0.0.1","port":5432}],"code":"ECONNREFUSED"},"msg":""}
AggregateError:
    at internalConnectMultiple (node:net:1114:18)
    at afterConnectMultiple (node:net:1667:5)
[ERROR] 18:33:59 AggregateError

Discussion