npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ciscode/config-kit

v0.3.1

Published

Typed env config for NestJS with Zod validation at startup. Fails fast on misconfiguration. Per-module namespace injection. Most foundational backend package.

Readme

@ciscode/config-kit

Typed, Zod-based environment configuration for NestJS.
Define your env shape once with a Zod schema — ConfigKit validates process.env at startup, fails loudly on misconfiguration, and gives you fully-typed, undefined-free config throughout your app.

Why config-kit?

| Problem with @nestjs/config | What config-kit does | | -------------------------------------------------- | ---------------------------------------------------------------- | | config.get('PORT') returns string \| undefined | config.get('PORT') returns number (inferred from Zod schema) | | Validation is optional and buried in code | App fails to start if any env var is wrong or missing | | Feature modules can't own their config slice | defineNamespace() gives each module its own typed scope |


📦 Installation

npm install @ciscode/config-kit zod @nestjs/common @nestjs/core

Peer dependencies (install alongside this package):

| Package | Version | | ---------------- | -------------- | | @nestjs/common | ^10 \|\| ^11 | | @nestjs/core | ^10 \|\| ^11 | | zod | ^3 \|\| ^4 |


Quick Start

1. Define your config shape with defineConfig

// src/app.config.ts
import { defineConfig } from "@ciscode/config-kit";
import { z } from "zod";

export const appConfig = defineConfig(
  z.object({
    // z.coerce.number() converts the env string "3000" → number 3000
    PORT: z.coerce.number().default(3000),
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
    NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
  }),
);

2. Register globally with ConfigModule.forRoot

// src/app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@ciscode/config-kit";
import { appConfig } from "./app.config";

@Module({
  imports: [
    // Validates process.env at startup — app never boots with bad config
    ConfigModule.forRoot(appConfig),
  ],
})
export class AppModule {}

3. Inject ConfigService for typed access

// src/server.service.ts
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@ciscode/config-kit";
import { appConfig } from "./app.config";

@Injectable()
export class ServerService {
  constructor(
    // Pass `typeof appConfig` so TypeScript knows the exact schema shape
    private readonly config: ConfigService<typeof appConfig>,
  ) {}

  getPort(): number {
    // Returns number — not string, not string|undefined
    return this.config.get("PORT");
  }

  getDatabaseUrl(): string {
    return this.config.get("DATABASE_URL");
  }
}

Async registration

Use registerAsync when the schema depends on another provider (e.g. a secrets vault):

ConfigModule.registerAsync({
  imports: [VaultModule],
  inject: [VaultService],
  useFactory: async (vault: VaultService) => {
    const secret = await vault.getSecret("JWT_SECRET");

    return defineConfig(
      z.object({
        JWT_SECRET: z.string().min(32).default(secret),
        PORT: z.coerce.number().default(3000),
      }),
    );
  },
});

Per-module config namespacing with defineNamespace

Feature modules can own and validate their own config slice independently, without polluting the root schema.

1. Define the namespace

// src/auth/auth.config.ts
import { defineNamespace } from "@ciscode/config-kit";
import { z } from "zod";

export const authConfig = defineNamespace(
  "auth",
  z.object({
    JWT_SECRET: z.string().min(32),
    JWT_EXPIRES_IN: z.string().default("7d"),
  }),
);

2. Register in the feature module

// src/auth/auth.module.ts
import { Module } from "@nestjs/common";
import { authConfig } from "./auth.config";
import { AuthService } from "./auth.service";

@Module({
  // authConfig.asProvider() validates this slice at startup and registers the DI token
  providers: [authConfig.asProvider(), AuthService],
})
export class AuthModule {}

3. Inject with @InjectConfig

// src/auth/auth.service.ts
import { Injectable } from "@nestjs/common";
import { InjectConfig } from "@ciscode/config-kit";
import { z } from "zod";
import { authConfig } from "./auth.config";

@Injectable()
export class AuthService {
  constructor(
    // 'auth' matches the namespace name in defineNamespace('auth', ...)
    @InjectConfig("auth")
    private readonly cfg: z.output<typeof authConfig.definition.schema>,
  ) {}

  getSecret(): string {
    return this.cfg.JWT_SECRET; // string — never undefined
  }
}

Note: AuthModule must import ConfigModule (or be in an app that does ConfigModule.forRoot()) so the NAMESPACE_REGISTRY_TOKEN is available.


Startup failure example

If any required env var is missing or invalid, ConfigKit throws ConfigValidationError before the app finishes booting:

ConfigValidationError: Config validation failed:
  • DATABASE_URL: Invalid url
  • JWT_SECRET: String must contain at least 32 character(s)

You can catch this in main.ts for custom formatting:

// src/main.ts
import { NestFactory } from "@nestjs/core";
import { ConfigValidationError } from "@ciscode/config-kit";
import { AppModule } from "./app.module";

async function bootstrap() {
  try {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
  } catch (err) {
    if (err instanceof ConfigValidationError) {
      console.error("❌ Invalid configuration:\n", err.message);
      // err.fields is a ZodIssue[] — inspect programmatically if needed
      process.exit(1);
    }
    throw err;
  }
}

bootstrap();

API Reference

defineConfig(schema)

Declares the env shape. Returns a ConfigDefinition<T> — pass it to ConfigModule.

const appConfig = defineConfig(z.object({ PORT: z.coerce.number().default(3000) }));

ConfigModule

| Method | Description | | ------------------------------------- | ---------------------------------------------------------- | | ConfigModule.forRoot(definition) | Global registration — ConfigService available everywhere | | ConfigModule.register(definition) | Non-global registration — scoped to the importing module | | ConfigModule.registerAsync(options) | Async registration with useFactory / inject |

ConfigService<TDef>

| Method | Returns | | ----------------- | --------------------------------------------------------------------- | | config.get(key) | Zod output type for key — never undefined unless schema allows it |

defineNamespace(namespace, schema)

Scoped config for feature modules. Returns NamespacedConfig<T>.

| Member | Description | | ------------------------------- | ------------------------------------------------------------- | | namespacedConfig.asProvider() | NestJS Provider — add to providers in your feature module | | @InjectConfig(namespace) | Parameter decorator — injects the validated slice |

Errors

| Class | When thrown | | ------------------------- | ------------------------------------------------------------------------------- | | ConfigValidationError | One or more env vars fail Zod validation at startup. Has .fields: ZodIssue[]. | | DuplicateNamespaceError | Same namespace name registered twice across the app. |


📝 Scripts

npm run build       # Compile TypeScript to dist/
npm run typecheck   # Type-check without emitting
npm test            # Run Jest test suite
npm run test:cov    # Tests + coverage report
npm run lint        # ESLint (--max-warnings=0)
npm run format      # Prettier check

📄 License

MIT — see LICENSE

🤝 Contributing

See CONTRIBUTING.md


Made with ❤️ by CisCode