Config Plugin
In modern microservices architecture, configuration management is the bedrock of stability. Hardcoding API keys or database URLs is a recipe for disaster. The Twelve-Factor App methodology explicitly states: Store config in the environment.
The ConfigPlugin is not just a wrapper around dotenv. It is a strict Validation Layer that ensures your application refuses to start if the environment does not meet the specified contract.
Concept: The Blueprint
Think of your application as a building. The Code is the bricks and mortar. The Configuration is the Blueprint.
If the blueprint says "This room needs a 220V outlet" (Database URL), but the site only provides 110V (Missing Env Var), the electrician (Karin) refuses to install the wiring rather than letting the house burn down later.
The Criticality of Use-Time vs. Boot-Time
Naive applications access process.env.DB_URL directly in their service methods. This leads to Runtime Failures. You might deploy a service and it runs fine for hours, until a specific user action triggers a code path that accesses a missing variable, causing a crash.
Karin's ConfigPlugin shifts this failure to Boot Time. If a required variable is missing or malformed, the application crashes immediately upon startup, preventing a broken deploy from ever receiving traffic.
schema Validation with Zod
We strongly recommend using Zod to define your configuration schema. This provides type coercion and sophisticated validation rules.
import { KarinFactory } from "@project-karin/core";
import { ConfigPlugin } from "@project-karin/config";
import { z } from "zod";
// Define the contract
const ConfigSchema = z.object({
// Transform "3000" (string) -> 3000 (number)
PORT: z.string().transform(Number).default("3000"),
// Validate URL format
DATABASE_URL: z.string().url(),
// Enforce specific environment values
NODE_ENV: z.enum(["development", "production", "test"]),
// Optional flag
ENABLE_DEBUG: z.string().transform((v) => v === "true").optional(),
});
// Infer the TypeScript type from the Schema
export type AppConfig = z.infer<typeof ConfigSchema>;
const app = await KarinFactory.create(adapter, {
plugins: [
new ConfigPlugin({
schema: ConfigSchema,
// If validation fails, the plugin throws a specialized error covering all missing keys
}),
],
});Dependency Injection: The ConfigService
Do not import variables globally. Use Dependency Injection to make your services testable. By injecting ConfigService, you can easily mock configuration values during unit tests without messing with process.env.
import { Service, Inject } from "@project-karin/core";
import { ConfigService } from "@project-karin/config";
@Service()
export class DatabaseService {
constructor(
// Inject the generic service
private config: ConfigService<AppConfig>
) {}
connect() {
// Fully typed access! TypeScript knows 'DATABASE_URL' exists and is a string.
const url = this.config.get("DATABASE_URL");
// No need to check for undefined here. The plugin guaranteed it exists at boot.
}
}Advanced Strategies
1. Inversion of Control (Serverless)
In serverless environments like Cloudflare Workers, environment variables are not global. They are passed as arguments to the fetch handler. The ConfigPlugin supports this Inversion of Control pattern.
// worker.ts
export default {
async fetch(req, env, ctx) {
const app = await KarinFactory.create(adapter, {
plugins: [
new ConfigPlugin({
env: env, // Inject the isolated environment object
schema: ConfigSchema,
}),
],
});
return app.fetch(req, env, ctx);
}
};2. Async Secret Loading
For enterprise setups, you might fetch secrets from AWS Secrets Manager or HashiCorp Vault. The load hook allows you to populate the config asynchronously.
new ConfigPlugin({
load: async () => {
const awsSecrets = await secretsManagerClient.getSecretValue({ SecretId: "prod/app" });
return JSON.parse(awsSecrets.SecretString);
},
schema: ConfigSchema, // Validation runs AFTER loading
})