Using ArkEnv with NestJS

Validate and type environment variables in NestJS with a fail-fast env module.

Edit on GitHub

Nest builds modules, providers, and database connections inside NestFactory.create(). ArkEnv validates environment variables when env.ts is first imported, before that call.

Setup

Declare the schema in src/env.ts and export env. Importing that module is the validation step. Controllers and services then read the typed object directly.

src/env.ts
import  from "@arkenv/core";

export const  = ({
  : "'development' | 'production' | 'test' = 'development'",
  PORT: "number.port = 3000",
  DATABASE_URL: "string",
});

Import env directly across your controllers, services, and modules:

src/app.service.ts
import { Injectable } from "@nestjs/common";
import { env } from "./env";

@Injectable()
export class AppService {
  getDatabaseUrl(): string {
    return env.DATABASE_URL;
  }
}

Fail-fast entrypoint

Import env at the top of src/main.ts, before NestFactory.create(), so a bad variable stops the process first.

src/main.ts
import { env } from "./env";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(env.PORT);
}
bootstrap();

Execution

ArkEnv does not load .env files. Nest has to put those values on process.env before src/env.ts evaluates. On Node.js 20.6+ and @nestjs/cli 11+, pass the native --env-file flag:

nest start --env-file .env

On @nestjs/cli 10 and earlier, build the app first and run node --env-file=.env dist/main.js.

In production, the platform or container injects the variables before the Node process starts.

Optional DI provider

Happy importing env directly?

Skip this section. A normal import is the default. A provider is for code that already injects config through Nest's container.

Register env with a Symbol token when a service should receive it through @Inject instead of importing the module:

src/env.provider.ts
import { env } from "./env";

export const ENV = Symbol("ENV");
export type Env = typeof env;

export const EnvProvider = {
  provide: ENV,
  useValue: env,
};

Register EnvProvider in your module's providers array (and exports if used across modules), then inject ENV into services using @Inject(ENV) private readonly env: Env.

Why avoid ConfigModule validation

Leave validationSchema alone

Do not pass an ArkEnv schema into @nestjs/config's ConfigModule.forRoot({ validationSchema }). Use src/env.ts instead.

Starting in @nestjs/config 12, validationSchema supports Standard Schema validators, but @nestjs/config passes raw string environment variables directly to the schema without automatic type coercion. A non-coercing validator (like ArkType's number.port or Zod's z.number()) rejects "3000" with a type error instead of parsing it. In @nestjs/config 11 and earlier, validationSchema doesn't support Standard Schema validators.

Using ArkEnv directly in src/env.ts handles string coercion automatically and fails immediately before NestJS initializes.

HTTP validation vs. environment validation

NestJS 12 supports Standard Schema pipes (such as StandardSchemaValidationPipe) for HTTP request payloads (@Body(), @Query()). HTTP validation pipes validate incoming requests at runtime and are distinct from application environment variable validation.