Get started / TypeScript (NestJS)
docuconf for TypeScript (NestJS)
You keep the class-validator class from the NestJS docs and ConfigModule.forRoot({ validate }). docuconf adds decorators for descriptions, secrets, URL schemes, durations and lists, and a validate function that checks everything at boot.
- Builds on
- @nestjs/config + class-validator
- Package
- @docuconf/nestjs (npm)
- Requires
- Node 22.12, NestJS 11 or 12
- Status
- v0.1 alpha, not yet released · conformance 131 of 134 (skips int64, json-schema)
- Source
- docuconf-js · the orders example
Not published yet
main branch. After the first release it will be npm install @docuconf/nestjs @nestjs/config class-validator class-transformer.The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.
1.Install
The packages are not on npm yet. Build them from the main branch and install the packed tarballs, with @nestjs/config and class-validator (Node 22.12 or later):
git clone https://github.com/Docuconf/docuconf-js ../docuconf-js
(cd ../docuconf-js && npm ci && npm run build && npm pack -w @docuconf/core -w @docuconf/nestjs)
npm install ../docuconf-js/docuconf-core-0.1.0.tgz ../docuconf-js/docuconf-nestjs-0.1.0.tgz @nestjs/config class-validator class-transformer2.Declare your configuration
The EnvironmentVariables class you would write anyway, with docuconf's @Describe, @Secret, @UrlSchemes, @List and @Duration. Property initializers are the defaults the contract records, and a TSDoc comment above a property adds its details.
// The service's configuration: the class-validator class @nestjs/config
// validates the environment with, plus docuconf's decorators for what
// class-validator has no word for (descriptions, secrets, URL schemes,
// durations, lists).
import { ArrayMinSize, IsEnum, IsInt, IsString, Max, MaxLength, Min } from "class-validator";
import { Describe, Duration, List, Secret, UrlSchemes, docuconfValidate } from "@docuconf/nestjs";
export enum LogLevel {
Debug = "debug",
Info = "info",
Warn = "warn",
Error = "error",
}
export class OrdersConfig {
@IsInt() @Min(1) @Max(65535) @Describe("Port the HTTP server listens on")
PORT: number = 8080;
@IsEnum(LogLevel) @Describe("Minimum log level emitted")
LOG_LEVEL: LogLevel = LogLevel.Info;
@Secret() @UrlSchemes("postgres") @MaxLength(2048) @Describe("Postgres connection string for the orders database")
DATABASE_URL!: string;
@List() @IsString({ each: true }) @ArrayMinSize(1) @Describe("Comma-separated CORS origins allowed to call the API")
ALLOWED_ORIGINS: string[] = ["http://localhost:3000"];
@Duration({ min: "1s", max: "5m", default: "30s" }) @Describe("Timeout for a single request")
REQUEST_TIMEOUT!: number;
/**
* Number of background order workers.
*
* Each worker holds one database connection, so keep this below the
* pool size of {@link DATABASE_URL}'s server.
*
* - Raise it when the order queue backs up.
* - Lower it when the database is the bottleneck.
*/
@IsInt() @Min(1) @Max(64) @Describe("Number of background order workers")
WORKER_COUNT: number = 4;
}
// exitOnError: on invalid configuration, print every problem and exit 1.
export const validate = docuconfValidate(OrdersConfig, { name: "orders", exitOnError: true });3.Load it at boot
Pass the validate function to ConfigModule.forRoot. ConfigService then gives typed values: REQUEST_TIMEOUT is milliseconds.
@Module({
// validate checks the whole environment once, at boot, and throws a
// DocuconfValidationError listing every violation.
imports: [ConfigModule.forRoot({ isGlobal: true, validate })],
controllers: [AppController],
})
export class AppModule {}const config = app.get<ConfigService<OrdersConfig, true>>(ConfigService);
const port = config.get("PORT", { infer: true });4.Run it, and see an error
Run the example with a valid environment:
npm run build && DATABASE_URL=postgres://orders:pw@localhost:5432/orders node dist/main.jsWith PORT=0 and no DATABASE_URL, the app does not start: with exitOnError: true, validate prints every problem with its error code and exits 1, without a stack trace. The same lines go to /dev/termination-log.
$ PORT=0 node dist/main.js
docuconf: 2 configuration problems:
- PORT [out_of_range]: must not be less than 1 (got "0")
- DATABASE_URL [missing_required]: required, but not set5.Test your configuration
validate is a plain function of the environment object, so a test calls it with its own object and never touches process.env. Under a test runner it throws a DocuconfValidationError even with exitOnError. This uses Node's test runner; Jest works the same way.
// Compiled with the app (npm run build), then: node --test dist/orders.config.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { DocuconfValidationError } from "@docuconf/nestjs";
import { validate } from "./orders.config.js";
// validate is the function ConfigModule.forRoot calls with the environment.
// A test passes its own object, so process.env is never read or changed.
test("defaults", () => {
const config = validate({ DATABASE_URL: "postgres://orders@db/orders" });
assert.equal(config.PORT, 8080);
assert.equal(config.REQUEST_TIMEOUT, 30_000);
});
test("rejects bad values", () => {
assert.throws(
() => validate({ PORT: "70000" }),
(e: unknown) =>
e instanceof DocuconfValidationError &&
e.violations.some((v) => v.input === "PORT" && v.code === "out_of_range") &&
e.violations.some((v) => v.input === "DATABASE_URL" && v.code === "missing_required"),
);
});node --test dist/orders.config.test.js6.Export the contract
Export loads the module without reading the environment. It compiles TypeScript with your own typescript package, with decorators on, as nest build does.
npx docuconf-nestjs export src/orders.config.ts --out contract.cue// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: {
name: "orders"
generator: {
language: "typescript"
sdk: "@docuconf/nestjs"
version: "0.1.0"
}
}
vars: {
ALLOWED_ORIGINS: {
type: "list"
description: "Comma-separated CORS origins allowed to call the API"
items: "string"
encoding: "csv"
separator: ","
minItems: 1
default: ["http://localhost:3000"]
}
DATABASE_URL: {
type: "url"
description: "Postgres connection string for the orders database"
required: true
secret: true
schemes: ["postgres"]
maxLength: 2048
}
LOG_LEVEL: {
type: "enum"
description: "Minimum log level emitted"
values: ["debug", "info", "warn", "error"]
default: "info"
}
PORT: {
type: "int"
description: "Port the HTTP server listens on"
min: 1
max: 65535
default: 8080
}
REQUEST_TIMEOUT: {
type: "duration"
description: "Timeout for a single request"
encoding: "go"
min: "1s"
max: "5m"
default: "30s"
}
WORKER_COUNT: {
type: "int"
description: "Number of background order workers"
details: "Each worker holds one database connection, so keep this below the\npool size of `DATABASE_URL`'s server.\n\n- Raise it when the order queue backs up.\n- Lower it when the database is the bottleneck."
min: 1
max: 64
default: 4
}
}
}The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.
7.NestJS
This SDK is the NestJS integration: ConfigModule.forRoot({ validate }), ConfigService<OrdersConfig, true> and { infer: true } work as the Nest docs describe. File inputs (TLS key pairs, config files) are decorators too, and a reload: "watch" file reloads in place. To declare with Zod instead, see @docuconf/t3.
Next
- The docuconf-js README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: ESM and CommonJS builds; NestJS 11 (CommonJS) and 12 (ESM). int is limited to the safe-integer range.
Every snippet on this page is compiled or run against docuconf-js's main branch in CI. Facts checked 2026-10-08.