### Boolean coercion [#1-boolean-coercion]
In JavaScript, any non-empty string is truthy—including `"false"`:
```ts title="./server.ts"
function getEnv() {
const debug = process.env.DEBUG;
if (debug === undefined) {
throw new Error("missing DEBUG");
}
return { debug };
}
const { debug } = getEnv();
if (debug) {
// always runs when DEBUG is set — including DEBUG=false
}
```
ArkEnv automatically coerces `"true"` and `"false"` into boolean primitives before validation.
### Numeric operations [#2-numeric-operations]
Because `process.env` only holds strings, numeric operations without coercion lead to concatenation:
```ts title="./server.ts"
const { port } = getEnv();
// port: string
listen(port);
port + 1; // "30001"
port > 1024; // lexicographic, not numeric
```
ArkEnv parses integers and numbers directly, validating ranges (`number.port` or `0 <= number <= 65535`) at boot.
### Empty string values [#3-empty-string-values]
When an environment variable is set to an empty string in `.env`:
```ini title=".env"
DATABASE_URL=
```
A helper checking `=== undefined` lets `""` pass without error. In ArkEnv, declaring `DATABASE_URL: "string.url"` fails the URL validation rule at boot because `""` is not a valid URL. Note that ArkEnv defaults to preserving empty strings; ignoring them requires the opt-in `emptyAsUndefined: true` option.
### Client/server secret boundary [#4-clientserver-secret-boundary]
When a helper exports `STRIPE_SECRET` from a shared module:
```ts title="./env.ts"
export const env = getEnv();
// env.STRIPE_SECRET accessed in a client component
```
Bundlers importing this helper into the browser either bundle the plaintext secret or evaluate it as `undefined`. ArkEnv's framework plugins (Next.js, Nuxt, Vite, Bun) enforce client prefixes (such as `NEXT_PUBLIC_` or `NUXT_PUBLIC_`) and block server keys from leaking to browser bundles.
## Graduating to ArkEnv [#graduating-to-arkenv]
Replace the helper with one schema in `./env.ts`. The keys stay. The
types stop being strings, and a bad value fails at import.
## Setup [#1-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.
```ts title="src/env.ts" twoslash
import arkenv from "@arkenv/core";
export const env = arkenv({
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
PORT: "number.port = 3000",
DATABASE_URL: "string",
});
```
```ts title="src/env.ts" twoslash
import arkenv from "@arkenv/standard";
import * as z from "zod";
export const env = arkenv({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.int().min(0).max(65535).default(3000),
DATABASE_URL: z.string(),
});
```
```ts title="src/env.ts" twoslash
import { arkenv } from "@arkenv/standard/valibot";
import * as v from "valibot";
export const env = arkenv({
NODE_ENV: v.optional(
v.picklist(["development", "production", "test"]),
"development",
),
PORT: v.optional(
v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535)),
3000,
),
DATABASE_URL: v.string(),
});
```
Import `env` directly across your controllers, services, and modules:
```ts title="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 [#2-fail-fast-entrypoint]
Import `env` at the top of `src/main.ts`, before
`NestFactory.create()`, so a bad variable stops the process first.
```ts title="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 [#3-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:
```bash
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 [#4-optional-di-provider]
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:
```ts title="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 [#5-why-avoid-configmodule-validation]
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.
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.
# Using ArkEnv with Nub (/docs/guides/use-with-nub)
ArkEnv is designed to work seamlessly with [Nub](https://nubjs.com/).
ArkEnv validates and types your environment variables; Nub loads
`.env*` files and runs TypeScript before your entrypoint starts.
Nub is not a required runtime for ArkEnv. Getting started and the
framework guides keep stock runners (`npx`, `node --env-file`,
framework CLIs). Use this page when your project already runs under Nub
— for example the [basic example](https://github.com/yamcodes/arkenv/tree/v1/examples/basic).
## Who does what [#who-does-what]
Nub injects `.env*` into `process.env` before Node starts (file runs,
`nub run`, `nub watch`, and `nubx`). ArkEnv does not load dotenv files
by itself. Calling `arkenv()` reads the active environment and fails
fast when values are missing or wrong.