Guide/guides

設定

Guren アプリの設定は config/ に置きます。ここに置くファイルは 2 種類です。

設定

Guren アプリの設定は config/ に置きます。ここに置くファイルは 2 種類です。

  • config/env.ts は、アプリが読む環境変数をすべて、型と必須かどうかを付けて宣言します。検証はアプリの起動時に 1 回だけ行われます。
  • config/<service>.tsdatabasecachemailsession など)は、検証済みの値からサービスの設定を組み立てます。どれも config 定義 を default export します。

どちらもただのデータです。import しただけでは接続も登録も起きず、それを行うのは createApp() です。

// src/app.ts
import { createApp } from '@guren/core'
import cache from '../config/cache.js'
import database from '../config/database.js'
import env from '../config/env.js'
import http from '../config/http.js'
import { registerWebRoutes } from '../routes/web.js'

const app = createApp({
  env,
  config: [database, http, cache],
  routes: registerWebRoutes,
})

export default app

create-guren-app はこの形で生成します。guren add cacheguren add mailguren add queueguren add storageguren add sessionguren add oauth は、config/env.ts があれば、それぞれの定義を config 配列に追加します(サービスプロバイダを使うアプリ を参照)。

環境変数を宣言する

config/env.tsdefineEnv() のスキーマを default export します。

// config/env.ts
import { defineEnv, Env, type InferEnv } from '@guren/core'

const env = defineEnv({
  APP_KEY: Env.string().secret().requiredInProduction()
    .describe('Signs cookies and encrypts session payloads.'),
  APP_URL: Env.url().requiredInProduction(),
  PORT: Env.port().default(3333),
  DATABASE_URL: Env.string().optional(),
  CACHE_STORE: Env.string().default('memory'),
  LOG_LEVEL: Env.enum(['debug', 'info', 'warn', 'error']).default('info'),
})

export default env

declare module '@guren/core' {
  interface AppEnv extends InferEnv<typeof env> {}
}

declare module のブロックで、検証済みの値に型が付きます。定義の中、コントローラの this.make('env')、データベースの接続リゾルバのどこで読んでも同じ型です。

ビルダー 受け付ける値 値の型
Env.string() 任意 string
Env.url() new URL() が解釈できる文字列 string
Env.number() 有限の数値 number
Env.port() 1 から 65535 の整数 number
Env.boolean() truefalse10(大文字小文字は問わない) boolean
Env.enum([...]) 列挙した文字列のいずれか その union 型
Env.custom(schema) 同期の Standard Schema(Zod、Valibot)が受け付ける値 スキーマの出力型

必須かどうか

修飾子を付けない変数は必須です。

修飾子 変数が未設定のとき
なし 起動に失敗する
.optional() 値は undefined
.default(value) 値は value
.requiredInProduction() NODE_ENVproduction なら起動に失敗し、それ以外では undefined

空の値は未設定として扱われます。 .envREDIS_URL= は、その行が無いのと同じです。デフォルトがあればそれが使われ、必須の変数なら起動に失敗します。.env.example は空の値を載せることが多く、ホスティングの管理画面で値を消した変数も空になります。空文字列そのものに意味がある場合は .allowEmpty() を付けてください。表示名をあえて空にしたメール送信者名がその例です。

.secret() を付けると、値がエラーメッセージに出なくなり、guren env:example はそのキーを空で書きます。.describe(text) の文は .env.example でキーの上のコメントになります。

検証に失敗したとき

起動は止まり、問題がまとめて表示されます。

[guren] Invalid environment (2 problems):
  APP_KEY  required, not set
  PORT     "80a" is not a port

宣言できない変数

NODE_ENVGUREN_* の変数は process.env から直接読むもので、defineEnv() に含めると例外になります。デプロイ用のビルドは process.env.NODE_ENV という式そのものをバンドル時に置き換えるので、検証済みの値を経由するとこの置き換えが効きません。GUREN_MCPGUREN_DOCS などの GUREN_* はフレームワーク側のゲートで、アプリのスキーマに左右されてはいけません。

config 定義

定義は検証済みの env を受け取り、サービスの設定を返します。

// config/cache.ts
import { defineCacheConfig } from '@guren/core'
import { createRedisClient } from '@guren/core/redis'

export default defineCacheConfig((env) => ({
  default: env.CACHE_STORE,
  stores: {
    memory: { driver: 'memory' },
    // A function, so the client is built only when CACHE_STORE selects this store.
    redis: { driver: 'redis', client: () => createRedisClient({ url: env.REDIS_URL }) },
  },
}))
ヘルパー バインドするキー 設定内容
defineDatabaseConfig(database, { seedOnBoot }) database 起動時に ORM を接続し、seedOnBoot が true でマイグレーションがあればシードも実行する
defineHttpConfig (HTTP カーネル) hostAuthorization
defineSessionConfig session SessionConfig
defineCacheConfig cache CacheConfig
defineMailConfig mail MailConfig
defineQueueConfig queue QueueConfig
defineStorageConfig storage StorageConfig
defineOAuthConfig oauth providers と、任意の stateStore

createApp() は、アプリのプロバイダより前に ConfigServiceProvider を登録します。このプロバイダは register() で env を検証して各定義のマネージャをバインドし、boot() では他のどのプロバイダよりも先に定義の起動処理(データベース接続)を実行します。そのため、アプリのプロバイダは自分の register() の中で cachemail を解決できます。config 配列の順番にも意味はありません。

1 つのキーを設定できるのは 1 か所だけです。 定義とプロバイダの両方が cache をバインドすると、どちらかが黙って勝つのではなく、両方の名前を挙げて起動に失敗します。サービスを定義に移したら、そのプロバイダは削除してください。

名前のチェックは定義の中で行います。マネージャはどんなストア名でも受け付け、実際に使われた時点で例外を投げます。それが数時間後のキューのジョブということもあるので、cache、queue、mail、storage の scaffold は起動時に確認しています。

// config/queue.ts
import { defineQueueConfig, MemoryDriver, SyncDriver } from '@guren/core'

const drivers = {
  sync: () => new SyncDriver(),
  memory: () => new MemoryDriver(),
}

export default defineQueueConfig((env) => {
  if (!Object.hasOwn(drivers, env.QUEUE_CONNECTION)) {
    throw new Error(
      `QUEUE_CONNECTION="${env.QUEUE_CONNECTION}" is not a declared driver. Declare it in config/queue.ts or use one of: ${Object.keys(drivers).join(', ')}.`,
    )
  }

  return { default: env.QUEUE_CONNECTION, drivers }
})

条件付きの設定は、コールバックの中に普通のコードとして書きます。

// config/oauth.ts
import { defineOAuthConfig, type OAuthProviderConfig, createGitHubOAuthProviderConfig } from '@guren/core'

export default defineOAuthConfig((env) => {
  const providers: Record<string, OAuthProviderConfig> = {}

  if (env.OAUTH_GITHUB_CLIENT_ID && env.OAUTH_GITHUB_CLIENT_SECRET && env.OAUTH_GITHUB_REDIRECT_URI) {
    providers.github = createGitHubOAuthProviderConfig({
      clientId: env.OAUTH_GITHUB_CLIENT_ID,
      clientSecret: env.OAUTH_GITHUB_CLIENT_SECRET,
      redirectUri: env.OAUTH_GITHUB_REDIRECT_URI,
    })
  }

  return { providers }
})

データベース接続

config/database.ts の名前付き export はそのまま残します。guren db:migrateguren db:seed が、アプリを起動せずにこれらを import するためです。接続リゾルバは、アプリの起動時には検証済みの env を受け取り、CLI から呼ばれたときは自分でスキーマを解析します。

// config/database.ts
import { createPostgresDatabase, defineDatabaseConfig } from '@guren/core'
import env from './env.js'

const database = createPostgresDatabase({
  migrationsFolder: new URL('../db/migrations', import.meta.url),
  seedersFolder: new URL('../db/seeders', import.meta.url),
  connectionString: (context) => (context?.env ?? env.parse(undefined, { mode: 'report' }).values).DATABASE_URL
    ?? 'postgres://guren:guren@localhost:54322/guren',
})

export const { getDatabase, migrateDatabase, closeDatabase, configureOrm, seedDatabase } = database

export default defineDatabaseConfig(database, { seedOnBoot: process.env.NODE_ENV !== 'production' })

mode: 'report' は例外を投げず、検証できた値を返します。そのため本番で guren db:migrate を実行しても、データベースに接続するためだけに APP_KEY を求められることはありません。

.env.example をそろえる

変数の一覧は config/env.ts が持ち、.env.example にも同じキーが並ぶようにします。

bunx guren env:example

このコマンドは、宣言済みで .env.example に無いキーを末尾に追加します。値にはデフォルト(シークレットは空)が入り、説明はコメントになります。すでにある行は書いたとおりに残ります。

bunx guren check --env

このコマンドは、2 つのファイルのキーがどちら向きにでも食い違っていれば失敗するので、CI のゲートに使えます。オプションなしの guren check も同じ比較を行い、あわせて createApp({ config }) に並んでいない config/<name>.ts も報告します。

コードから環境変数を読む

process.env ではなく、コンテナから検証済みの値を読みます。

import { Controller } from '@guren/core'

export default class AdminController extends Controller {
  async index() {
    const { ADMIN_EMAIL } = this.make('env')
    // ...
  }
}

scaffold したアプリでは、lint ルール guren/no-unvalidated-env-readapp/config/routes/src/modules/ の中の process.env.X を報告します。XNODE_ENVGUREN_* なら対象外です。bin/drizzle.config.ts はアプリより先に動くので対象にしていません。意図して直接読む箇所は、その行でルールを無効にし、理由を書きます。

// oxlint-disable-next-line guren/no-unvalidated-env-read -- CI switch, not app config
const secureCookies = process.env.NODE_ENV === 'production' && !process.env.CI

Cloudflare Workers

Workers では、wrangler の vars とシークレットはエントリポイントの env 引数で渡され、process.env に入るとは限りません。@guren/plugin-cloudflare はアプリの起動前にこの引数をバインドし、スキーマはまずそこからキーを探し、無ければ process.env を読みます。同じ config/env.ts が、手を加えずにローカルでも Workers でも動きます。

テスト

TestApp.fromApp(app) は実際の src/app.ts を定義ごと起動するので、機能テストは本番と同じ設定で動きます。

process.env を触らずに変数を 1 つだけ差し替えたいときは、スキーマと上書きする値を TestApp.create() に渡します。envSourceprocess.env より先に読まれ、'' を渡すとその変数は未設定になります。

import { TestApp } from '@guren/testing'
import env from '../config/env.js'

const app = await TestApp.create({
  env,
  envSource: { CACHE_STORE: 'memory', APP_URL: '' },
  providers: [ReportProvider],
})

上書きした値が不正なら、create()EnvValidationError で reject します。

サービスプロバイダを使うアプリ

config 定義より前に作られたアプリは、サービスをプロバイダで設定しています(CacheProviderMailProviderconfig/session.ts を読む SessionProvider など)。これらはそのまま動き、移行は必須ではありません。bunx guren doctor --next はこうしたプロバイダを見つけ、書くべき定義を、プロバイダが読んでいた値と宣言すべき変数つきで表示します。移すときの手順は次のとおりです。

  1. config/env.ts を追加し、プロバイダが読んでいる変数を宣言する。
  2. 対応する define*Config ヘルパーで config/<service>.ts を書き、process.env の代わりに env を読む。
  3. 同じ変更の中で、それを createApp({ config }) に加え、プロバイダを providers から削除する。
  4. bunx guren env:examplebunx guren check を実行する。

config/env.ts があり、そのキーをバインドするプロバイダが無ければ、次に guren add <service> で追加するサービスは定義の形で生成されます。