Skip to content
docuconf
docuconf on GitHub

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

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the 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-transformer

2.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.

src/orders.config.ts
// 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.

src/app.module.ts
@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 {}
src/main.ts
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.js

With 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.

Terminal
$ 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 set

5.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.

src/orders.config.test.ts
// 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.js

6.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
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

Every snippet on this page is compiled or run against docuconf-js's main branch in CI. Facts checked 2026-10-08.