From andercore-toolkit-services
Type-safe configuration with Joi validation. Use when setting up environment variables, validating config, accessing settings.
npx claudepluginhub andercore-labs/claudes-kitchen --plugin andercore-toolkit-servicesThis skill uses the workspace's default tool permissions.
| Need | Pattern | File |
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
| Need | Pattern | File |
|---|---|---|
| Define schema | Joi.object({ ... }) | app.module.ts:10 |
| Inject config | @Inject(ConfigService) | service.ts:8 |
| Get value | config.get<T>('KEY') | service.ts:15 |
Environment variables | config validation | application settings | type-safe config
app.module.ts:
import * as Joi from 'joi'
import { AndercoreToolkitModule } from '@andercore/toolkit'
@Module({
imports: [
AndercoreToolkitModule.forRoot({
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number()
.port()
.default(3000),
DATABASE_URL: Joi.string()
.uri()
.required(),
API_KEY: Joi.string()
.min(32)
.required(),
REDIS_HOST: Joi.string()
.hostname()
.default('localhost'),
REDIS_PORT: Joi.number()
.port()
.default(6379),
LOG_LEVEL: Joi.string()
.valid('debug', 'info', 'warn', 'error')
.default('info'),
FEATURE_FLAG_NEW_UI: Joi.boolean()
.default(false)
}),
validationOptions: {
allowUnknown: true, // Allow non-validated env vars
abortEarly: false // Collect all validation errors
}
})
]
})
export class AppModule {}
| Method | Effect | Example |
|---|---|---|
.required() | Fail on missing | API_KEY: Joi.string().required() |
.default(val) | Fallback value | PORT: Joi.number().default(3000) |
.valid(...vals) | Enum constraint | NODE_ENV: Joi.string().valid('dev', 'prod') |
.min(n) | Minimum length/value | API_KEY: Joi.string().min(32) |
.max(n) | Maximum length/value | PORT: Joi.number().max(65535) |
.uri() | Valid URI | DATABASE_URL: Joi.string().uri() |
.hostname() | Valid hostname | REDIS_HOST: Joi.string().hostname() |
.port() | Valid port (1-65535) | PORT: Joi.number().port() |
.email() | Valid email | ADMIN_EMAIL: Joi.string().email() |
.pattern(regex) | Regex match | API_KEY: Joi.string().pattern(/^[A-Z0-9]+$/) |
.when(condition) | Conditional | REDIS_TLS: Joi.boolean().when('NODE_ENV', ...) |
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
@Injectable()
export class UserService {
constructor(private config: ConfigService) {}
async connect(): Task<Connection, ConnectionError> {
const url = this.config.get<string>('DATABASE_URL')
const timeout = this.config.getOrThrow<number>('DB_TIMEOUT')
return this.db.connect({ url, timeout })
}
isFeatureEnabled(): boolean {
return this.config.get<boolean>('FEATURE_FLAG_NEW_UI', false)
}
}
| Method | Behavior | Example |
|---|---|---|
get<T>(key) | Returns T | undefined | config.get<string>('API_KEY') |
get<T>(key, defaultValue) | Returns T | defaultValue | config.get<number>('PORT', 3000) |
getOrThrow<T>(key) | Returns T or throws | config.getOrThrow<string>('API_KEY') |
✓ DO - Type-safe access:
const port = this.config.get<number>('PORT', 3000)
const apiKey = this.config.getOrThrow<string>('API_KEY')
const features = this.config.get<string[]>('ENABLED_FEATURES', [])
✗ DON'T - Direct process.env access:
const port = process.env.PORT // ✗ VIOLATION (string | undefined)
const apiKey = process.env.API_KEY // ✗ VIOLATION (no validation)
File structure:
.env → local dev (gitignored)
.env.test → tests (auto-loaded in NODE_ENV=test)
.env.example → team template (committed)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=your-secret-api-key-here
REDIS_HOST=localhost
REDIS_PORT=6379
LOG_LEVEL=debug
FEATURE_FLAG_NEW_UI=true
# OTEL configuration
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_RESOURCE_ATTRIBUTES=service.name=my-service,service.version=1.0.0
# External APIs
PAYMENT_API_URL=https://api.payment-provider.com
PAYMENT_API_KEY=payment-secret-key
NODE_ENV=test
PORT=3001
DATABASE_URL=postgresql://localhost:5432/mydb_test
API_KEY=test-api-key
REDIS_HOST=localhost
REDIS_PORT=6380
LOG_LEVEL=error
FEATURE_FLAG_NEW_UI=false
# Test-specific overrides
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Application
NODE_ENV=development
PORT=3000
# Database
DATABASE_URL=postgresql://localhost:5432/mydb
# API Keys (REPLACE WITH ACTUAL VALUES)
API_KEY=your-api-key-here
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# Logging
LOG_LEVEL=info
# Feature Flags
FEATURE_FLAG_NEW_UI=false
# OpenTelemetry
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_RESOURCE_ATTRIBUTES=service.name=my-service,service.version=1.0.0
# External APIs
PAYMENT_API_URL=https://api.payment-provider.com
PAYMENT_API_KEY=your-payment-api-key
.env
.env.local
.env.*.local
AndercoreToolkitModule.forRoot({
validationSchema: Joi.object({ ... }),
validationOptions: {
allowUnknown: true, // Allow non-schema env vars
abortEarly: false, // Collect all errors
stripUnknown: false, // Keep non-schema vars
presence: 'required' // Default requirement level
}
})
| Option | Default | Effect |
|---|---|---|
| allowUnknown | false | Allow env vars not in schema |
| abortEarly | false | Stop on first error vs collect all |
| stripUnknown | false | Remove non-schema vars |
| presence | 'optional' | Default requirement level |
const getValidationSchema = () => {
const baseSchema = {
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
PORT: Joi.number().port().default(3000),
DATABASE_URL: Joi.string().uri().required()
}
if (process.env.NODE_ENV === 'production') {
return Joi.object({
...baseSchema,
API_KEY: Joi.string().min(64).required(), // Stricter in prod
HTTPS_ENABLED: Joi.boolean().valid(true).required(), // Force HTTPS
RATE_LIMIT: Joi.number().min(100).required()
})
}
return Joi.object({
...baseSchema,
API_KEY: Joi.string().min(32).default('dev-key'),
HTTPS_ENABLED: Joi.boolean().default(false),
RATE_LIMIT: Joi.number().default(1000)
})
}
@Module({
imports: [
AndercoreToolkitModule.forRoot({
validationSchema: getValidationSchema()
})
]
})
export class AppModule {}
// Type definition
type AppConfig = {
database: {
url: string
maxConnections: number
}
redis: {
host: string
port: number
tls: boolean
}
}
// Schema
const validationSchema = Joi.object({
DATABASE_URL: Joi.string().uri().required(),
DATABASE_MAX_CONNECTIONS: Joi.number().default(10),
REDIS_HOST: Joi.string().hostname().default('localhost'),
REDIS_PORT: Joi.number().port().default(6379),
REDIS_TLS: Joi.boolean().default(false)
})
// Service
@Injectable()
export class ConfigAdapter {
constructor(private config: ConfigService) {}
getDatabaseConfig() {
return {
url: this.config.getOrThrow<string>('DATABASE_URL'),
maxConnections: this.config.get<number>('DATABASE_MAX_CONNECTIONS', 10)
}
}
getRedisConfig() {
return {
host: this.config.get<string>('REDIS_HOST', 'localhost'),
port: this.config.get<number>('REDIS_PORT', 6379),
tls: this.config.get<boolean>('REDIS_TLS', false)
}
}
}
DATABASE_URL: Joi.string()
.uri({ scheme: ['postgresql', 'postgres'] })
.required()
DATABASE_MAX_CONNECTIONS: Joi.number()
.integer()
.min(1)
.max(100)
.default(10)
DATABASE_TIMEOUT: Joi.number()
.integer()
.min(1000)
.default(30000)
DATABASE_SSL: Joi.boolean()
.default(false)
API_KEY: Joi.string()
.min(32)
.required()
API_SECRET: Joi.string()
.min(64)
.required()
API_TIMEOUT: Joi.number()
.integer()
.min(1000)
.max(60000)
.default(30000)
API_RETRY_ATTEMPTS: Joi.number()
.integer()
.min(0)
.max(5)
.default(3)
FEATURE_FLAG_NEW_UI: Joi.boolean()
.default(false)
FEATURE_FLAG_BETA_FEATURES: Joi.boolean()
.default(false)
FEATURE_FLAG_MAINTENANCE_MODE: Joi.boolean()
.default(false)
OTEL_EXPORTER_OTLP_ENDPOINT: Joi.string()
.uri()
.required()
OTEL_RESOURCE_ATTRIBUTES: Joi.string()
.pattern(/^([^=]+=[^=,]+(,[^=]+=[^=,]+)*)?$/)
.required()
OTEL_LOG_LEVEL: Joi.string()
.valid('debug', 'info', 'warn', 'error')
.default('info')
→ typescript-services:test-code-recipe for patterns
Mock ConfigService:
const mockConfig = {
get: jest.fn((key, defaultValue) => defaultValue),
getOrThrow: jest.fn((key) => 'mock-value')
}
CHECK:
- [ ] Joi schema in AndercoreToolkitModule.forRoot()?
- [ ] .required() on all critical fields?
- [ ] NO .default() on secrets (API keys, passwords)?
- [ ] .env.example committed with placeholders?
- [ ] .env in .gitignore?
- [ ] ConfigService.get<T>() used everywhere?
- [ ] NO process.env.VAR direct access in services?
- [ ] Type parameters provided for get<T>()?
- [ ] .env.test for test environment?
- [ ] Validation options configured (allowUnknown, abortEarly)?
VIOLATIONS (CRITICAL):
- process.env.VAR in service/domain → CRITICAL
- Secret with .default() in schema → CRITICAL
- Missing .env.example → MEDIUM
- No type parameter in config.get() → MEDIUM
- .env committed to git → CRITICAL
- Missing .required() on critical fields → HIGH
PASS → DONE | FAIL → cite violations with file:line
Config access allowed in all layers:
✗ Domain: NO config access (pure business logic)
See backend-services:hexagonal-architecture-recipe for configuration injection patterns