13. Configuration
Status: Runnable. Everything on this page works today.
Apps read their settings from environment variables: ports, database URLs, API keys. defineConfig checks them with the same schemas you use for routes, gives you a typed object, and stops the app at startup when something is missing or wrong.
Defining the config
// src/config.ts
import { defineConfig, type TokenType } from '@avijs/core';
import * as v from 'valibot';
const env = v.object({
PORT: v.optional(v.pipe(v.string(), v.regex(/^\d+$/), v.transform(Number)), '3000'),
DATABASE_URL: v.pipe(v.string(), v.url()),
LOG_LEVEL: v.optional(v.picklist(['info', 'warn', 'error']), 'info'),
});
export const Config = defineConfig(env, { envFile: '.env' });
export type Config = TokenType<typeof Config>; // the validated valuesAny Standard Schema library works; with Zod the same is z.object({ PORT: z.coerce.number() ... }). The type of the config is the schema's output: here { PORT: number; DATABASE_URL: string; LOG_LEVEL: 'info' | 'warn' | 'error' }.
Projects created with npm create avi and a validator already have a src/config.ts with PORT. The Config type next to the token names the values, so config: Config reads like any other dependency.
Using it
Config is an injection token. Import Config.module where it is needed and inject it like any other dependency:
@Injectable()
export class Database {
// Config is a token named like its type, so avi dev infers it; no list needed.
constructor(private readonly config: Config) {}
connect() {
return createPool(this.config.DATABASE_URL);
}
}
@Module({ imports: [Config.module], providers: [Database], exports: [Database] })
export class DatabaseModule {}A database client is usually built by a factory instead. defineFactory types its parameters from inject (chapter 3):
export const Db = createToken<PrismaClient>('Db');
@Module({
imports: [Config.module],
providers: [
defineFactory({
provide: Db,
inject: [Config],
useFactory: async (config) => {
const db = new PrismaClient({ adapter: new PrismaLibSql({ url: config.DATABASE_URL }) });
await db.$connect();
return db;
},
dispose: (db) => db.$disconnect(),
}),
],
exports: [Db],
})
export class DatabaseModule {}Every module that imports Config.module shares one validated instance. In main.ts, read it from the app:
const app = await AviFactory.create(AppModule);
await app.listen(app.get(Config).PORT);Outside the app, in a script or a test, await Config.load() reads and validates the config directly. To register the provider yourself instead of importing the module, use Config.provider.
When something is wrong
The app does not start. The error lists every problem, by variable name:
ConfigError: Invalid configuration: DATABASE_URL: Invalid URL: Received "[redacted]"; LOG_LEVEL: Invalid typeValues are never included, even when the schema library quotes them in its message or a transform changed them first: a quoted value is replaced by "[redacted]", while the options a schema expects ("info" | "warn") stay. A schema that throws (a JSON.parse transform on bad input, say) reports the schema threw while reading the variables, without the original error, whose message may quote the value. So a mistyped secret cannot end up in your logs. ConfigError also has the problems as data, in error.issues.
Where values come from
- Environment variables that are set, such as those from your platform or
PORT=8080 npm start. - The
envFile, if you pass one and it exists. Variables that are already set win over it.
The file is read with Node's own .env parser; Avi.js does not need dotenv, and it never changes process.env. A missing file is skipped. Make it required with { envFile: { path: '.env', required: true } }.
Keep .env out of git (new projects already ignore it) and use your platform's secrets in production.
Testing
Pass env to validate a fixed set of values instead of process.env:
const TestConfig = defineConfig(env, { env: { DATABASE_URL: 'postgres://localhost/test' } });Or replace the whole config in a test with overrides:
const client = await createTestClient(AppModule, {
overrides: [{ provide: Config, useValue: { PORT: 0, DATABASE_URL: 'x', LOG_LEVEL: 'info' } }],
});