Skip to content

Configuration

The @forinda/kickjs-config package provides Zod-validated environment configuration with caching and an injectable service for accessing values throughout your application.

Defining an Environment Schema

Use defineEnv() to extend the base schema with your application-specific variables. The base schema includes PORT, NODE_ENV, and LOG_LEVEL:

ts
import { z } from 'zod'
import { defineEnv } from '@forinda/kickjs-config'

const envSchema = defineEnv((base) =>
  base.extend({
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
    REDIS_URL: z.string().url().optional(),
  })
)

The base schema provides these defaults:

VariableTypeDefault
PORTnumber3000
NODE_ENV'development' | 'production' | 'test''development'
LOG_LEVELstring'info'

Loading and Accessing Environment Variables

loadEnv

Parse and validate process.env against your schema. The result is cached -- subsequent calls return the same object without re-parsing:

ts
import { loadEnv } from '@forinda/kickjs-config'

const env = loadEnv(envSchema)
console.log(env.DATABASE_URL) // fully typed

If called without a schema, loadEnv() uses the base schema only.

getEnv

Retrieve a single variable from the cached config:

ts
import { getEnv } from '@forinda/kickjs-config'

const port = getEnv('PORT') // uses cached env, falls back to base schema

resetEnvCache

Clear the cached config. Useful in tests when you need to reload with different environment values:

ts
import { resetEnvCache, loadEnv } from '@forinda/kickjs-config'

resetEnvCache()
process.env.PORT = '4000'
const env = loadEnv(envSchema) // re-parsed with new PORT

Accessing Config in Services

There are two approaches for accessing environment config via DI. Choose based on whether you need full type safety.

Option 1: ConfigService (untyped, quick)

ConfigService is a built-in injectable singleton that wraps loadEnv() with the base schema. It works without any setup but does not provide typed keys or return values — you must cast manually:

ts
import { Service, Autowired } from '@forinda/kickjs-core'
import { ConfigService } from '@forinda/kickjs-config'

@Service()
class DatabaseService {
  @Autowired() private config!: ConfigService

  connect() {
    const url = this.config.get<string>('DATABASE_URL') // manual cast, no autocomplete
  }
}

createConfigService() creates an injectable service class bound to your Zod schema. Keys autocomplete and return values are inferred from the schema — no manual casting:

ts
// src/config/env.ts
import { z } from 'zod'
import { defineEnv, loadEnv, createConfigService } from '@forinda/kickjs-config'

export const envSchema = defineEnv((base) =>
  base.extend({
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
    REDIS_URL: z.string().url().optional(),
  })
)

// Direct access (no DI needed)
export const env = loadEnv(envSchema)
env.DATABASE_URL  // string — fully typed
env.JWT_SECRET    // string — fully typed
env.REDIS_URL     // string | undefined — fully typed

// Injectable service (for DI)
export const AppConfigService = createConfigService(envSchema)
export type AppConfigService = InstanceType<typeof AppConfigService>

Then inject it in any service or controller:

ts
import { Service, Autowired } from '@forinda/kickjs-core'
import { AppConfigService } from '../config/env'

@Service()
class DatabaseService {
  @Autowired() private config!: AppConfigService

  connect() {
    const url = this.config.get('DATABASE_URL')  // string — autocompletes!
    const bad = this.config.get('NOPE')           // TS error — key doesn't exist
  }
}

Which to use?

loadEnv()ConfigServicecreateConfigService()
Type safetyFullNone (manual cast)Full
DI injectableNoYesYes
Key autocompleteYesNoYes
Best forModule-scope accessQuick prototypingProduction services

Available Methods

Both ConfigService and createConfigService instances provide:

MethodReturnDescription
get(key)typed valueGet a single env variable by key
getAll()Readonly<TEnv>Get a frozen copy of all config values
reload()voidRe-read .env and re-validate (for HMR)
isProduction()booleanNODE_ENV === 'production'
isDevelopment()booleanNODE_ENV === 'development'
isTest()booleanNODE_ENV === 'test'

@Value Decorator

The @Value decorator injects an environment variable directly into a class property. It is evaluated lazily -- the value is read from process.env at access time, not at decoration time.

ts
import { Service, Value } from '@forinda/kickjs-core'

@Service()
class MailService {
  @Value('SMTP_HOST', 'localhost')
  private smtpHost!: string

  @Value('SMTP_PORT', '587')
  private smtpPort!: string

  @Value('SMTP_API_KEY')
  private apiKey!: string // throws if SMTP_API_KEY is not set
}

If no default is provided and the environment variable is missing, accessing the property throws an error to catch misconfiguration early:

@Value('SMTP_API_KEY'): Environment variable "SMTP_API_KEY" is not set and no default was provided.

The container wires up @Value properties through Object.defineProperty getters during instance creation, alongside @Autowired property injection.

Released under the MIT License.