Configuration

Manage environment variables with validated, type-safe configuration.

MiiaJS provides a ConfigModule for loading and validating environment variables with any ZodLike schema.

Setup

import { Module } from '@miiajs/core'
import { ConfigModule } from '@miiajs/config'
import { z } from 'zod'

const EnvSchema = z.object({
  PORT: z.string().default('3000'),
  DATABASE_URL: z.string(),
  JWT_SECRET: z.string(),
  DEBUG: z.string().default('false'),
})

@Module({
  imports: [
    ConfigModule.configure({ schema: EnvSchema }),
  ],
})
class AppModule {}

ConfigModule.configure() validates process.env against your schema at startup. If validation fails, the application throws with detailed error messages.

Using ConfigService

Inject ConfigService to access validated values:

import { Injectable, inject } from '@miiajs/core'
import { ConfigService } from '@miiajs/config'

@Injectable()
class DatabaseService {
  private configService = inject(ConfigService)

  connect() {
    const url = this.configService.getOrThrow('DATABASE_URL')
    const port = this.configService.get('PORT') ?? '3000'
    // ...
  }
}

API

MethodReturnsOn missing key
get(key)T[K] | undefinedReturns undefined
getOrThrow(key)T[K]Throws Error

Factory configuration

Use a factory function to resolve config from DI:

import { DrizzleModule } from '@miiajs/drizzle'

@Module({
  imports: [
    ConfigModule.configure({ schema: EnvSchema }),
    DrizzleModule.configure((resolve) => {
      const config = resolve(ConfigService)
      return {
        dialect: 'postgres',
        connection: { url: config.getOrThrow('DATABASE_URL') },
      }
    }),
  ],
})
class AppModule {}

The resolve function gives access to the DI container, allowing modules to depend on each other's configuration.

Custom env source

By default, ConfigModule reads from process.env. You can pass a custom source:

ConfigModule.configure({
  schema: EnvSchema,
  env: {
    PORT: '8080',
    DATABASE_URL: 'postgres://localhost:5432/test',
    JWT_SECRET: 'test-secret',
  },
})

This is useful for testing or when loading from a custom source.

Schema requirements

The schema must implement a safeParse() method (ZodLike interface):

interface ZodLike<T = any> {
  safeParse(data: unknown):
    | { success: true; data: T }
    | { success: false; error: { issues: { message: string; path?: (string | number)[] }[] } }
}

@miiajs/config does not declare zod as a peer dependency - bring your own validator. Any library or custom object that exposes a safeParse method matching the shape above works.

Using non-Zod validators

Zod implements ZodLike natively, but you can plug in any modern validator. The two most common alternatives:

Valibot

Valibot's safeParse returns { success, output, issues } - one tiny adapter:

import * as v from 'valibot'
import { ConfigModule } from '@miiajs/config'

const schema = v.object({
  PORT: v.pipe(v.string(), v.transform(Number), v.number()),
  DATABASE_URL: v.string(),
})

ConfigModule.configure({
  schema: {
    safeParse: (data) => {
      const r = v.safeParse(schema, data)
      return r.success
        ? { success: true, data: r.output }
        : { success: false, error: { issues: r.issues.map((i) => ({ message: i.message, path: i.path?.map((p) => p.key) })) } }
    },
  },
})

ArkType

ArkType's type(...) already returns either the parsed value or an error array - similar shape, similar adapter:

import { type } from 'arktype'
import { ConfigModule } from '@miiajs/config'

const schema = type({
  PORT: 'string.numeric.parse',
  DATABASE_URL: 'string',
})

ConfigModule.configure({
  schema: {
    safeParse: (data) => {
      const r = schema(data)
      return r instanceof type.errors
        ? { success: false, error: { issues: r.map((e) => ({ message: e.message, path: e.path })) } }
        : { success: true, data: r }
    },
  },
})

Hand-rolled

For simple cases you don't need a library at all:

ConfigModule.configure({
  schema: {
    safeParse: (data: any) => {
      if (!data.DATABASE_URL) {
        return { success: false, error: { issues: [{ message: 'DATABASE_URL is required', path: ['DATABASE_URL'] }] } }
      }
      return { success: true, data: { ...data, PORT: Number(data.PORT ?? 3000) } }
    },
  },
})