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

@hexaijs/utils

v0.1.0

Published

A collection of utility functions and value objects for hexai projects

Readme

@hexaijs/utils

Configuration management utilities for hexai applications

Overview

@hexaijs/utils provides utilities for managing application configuration in a type-safe, validated manner. The package currently focuses on configuration management, offering a declarative approach to reading and validating environment variables.

The configuration system solves several common problems:

  1. Type Safety - Environment variables are strings, but your application needs typed values (numbers, booleans, objects). The config system handles transformation and inference automatically.
  2. Validation at Startup - Missing or invalid configuration is detected immediately when your application starts, not when the code path is first executed.
  3. Singleton Management - Configuration is resolved once and cached, with special handling for test environments.
  4. Extensibility - Custom config specs allow database packages and other infrastructure to integrate seamlessly.

Installation

npm install @hexaijs/utils

Core Concepts

defineConfig

defineConfig creates a typed configuration getter that resolves and validates configuration on first access.

import {
    defineConfig,
    env,
    envOptional,
    envNumber,
    envBoolean,
} from "@hexaijs/utils/config";

export const getConfig = defineConfig({
    // Required string
    apiKey: env("API_KEY"),

    // Optional with default
    logLevel: envOptional("LOG_LEVEL", "info"),

    // Required number
    port: envNumber("PORT"),

    // Boolean with default
    debug: envBoolean("DEBUG", false),

    // Static values are also supported
    appName: "my-app",
    maxRetries: 3,
});

// Usage
const config = getConfig();
config.apiKey;      // string
config.logLevel;    // string | undefined
config.port;        // number
config.debug;       // boolean
config.appName;     // "my-app" (literal type preserved)
config.maxRetries;  // 3 (literal type preserved)

The returned getter function caches the resolved config (singleton pattern). In test environments (NODE_ENV=test), the config is recreated on each call to reflect environment changes between tests.

Environment Variable Specs

Builder functions for different data types:

| Function | Type | Description | |----------|------|-------------| | env(key) | string | Required string | | envOptional(key, default?) | string \| undefined | Optional string with default | | envNumber(key) | number | Required number | | envNumberOptional(key, default?) | number \| undefined | Optional number | | envBoolean(key, default) | boolean | Boolean ("true", "1" → true) | | envJson<T>(key) | T | Required JSON-parsed object | | envJsonOptional<T>(key, default?) | T \| undefined | Optional JSON object |

import {
    defineConfig,
    env,
    envNumber,
    envBoolean,
    envJson,
} from "@hexaijs/utils/config";

const getConfig = defineConfig({
    // Required - throws ConfigValidationError if missing
    databaseUrl: env("DATABASE_URL"),

    // Number transformation
    port: envNumber("PORT"),

    // Boolean - recognizes "true" and "1"
    enableMetrics: envBoolean("ENABLE_METRICS", false),

    // JSON parsing with type inference
    rateLimits: envJson<{ requests: number; window: number }>("RATE_LIMITS"),
});

ConfigValidationError

When required environment variables are missing or transformation fails, defineConfig collects all errors and throws a single ConfigValidationError.

import { defineConfig, env, ConfigValidationError } from "@hexaijs/utils/config";

const getConfig = defineConfig({
    apiKey: env("API_KEY"),
    secretKey: env("SECRET_KEY"),
    databaseUrl: env("DATABASE_URL"),
});

try {
    const config = getConfig();
} catch (e) {
    if (e instanceof ConfigValidationError) {
        console.error("Configuration errors:");
        e.errors.forEach(err => console.error(`  - ${err}`));
        // Config validation failed:
        //   - Missing required env: API_KEY
        //   - Missing required env: SECRET_KEY
        //   - Missing required env: DATABASE_URL
        process.exit(1);
    }
    throw e;
}

All validation errors are collected before throwing, so you see every missing variable at once rather than fixing them one at a time.

Custom ConfigSpec

The ConfigSpec interface allows you to create custom configuration resolvers. Infrastructure packages like @hexaijs/postgres use this to provide database configuration specs.

import type { ConfigSpec } from "@hexaijs/utils/config";

// Example: Custom config spec for Redis
class RedisSpec implements ConfigSpec<RedisConfig> {
    readonly _type = "redis";

    constructor(private prefix: string) {}

    resolve(errors: string[]): RedisConfig | undefined {
        const host = process.env[`${this.prefix}_HOST`];
        const port = process.env[`${this.prefix}_PORT`];

        if (!host) {
            errors.push(`Missing required env: ${this.prefix}_HOST`);
            return undefined;
        }

        return {
            host,
            port: port ? parseInt(port) : 6379,
        };
    }
}

// Usage with defineConfig
const getConfig = defineConfig({
    redis: new RedisSpec("REDIS"),
    apiKey: env("API_KEY"),
});

The resolve method should push error messages to the errors array instead of throwing, allowing all errors to be collected.

DatabaseConfig Interface

A common interface that database configuration classes implement, enabling polymorphic handling of different databases.

import type { DatabaseConfig } from "@hexaijs/utils/config";

// PostgresConfig from @hexaijs/postgres implements this interface
interface DatabaseConfig {
    readonly host: string;
    readonly port: number;
    readonly database: string;
    readonly user: string;
    readonly password?: string;
    toString(): string;  // Returns connection URL
}

This interface is used by database packages to provide type-safe configuration. See @hexaijs/postgres for a complete implementation.

Loading .env Files

The loadEnvFiles function loads environment files following a standard precedence order.

import { loadEnvFiles, defineConfig, env } from "@hexaijs/utils/config";

// Option 1: Load env files before defining config
loadEnvFiles();

const getConfig = defineConfig({
    apiKey: env("API_KEY"),
});

// Option 2: Use loadEnv option in defineConfig
const getConfig = defineConfig(
    { apiKey: env("API_KEY") },
    { loadEnv: true }
);

File loading order (later files override earlier):

  1. .env - Default values
  2. .env.local - Local overrides (gitignored)
  3. .env.{NODE_ENV} - Environment-specific (e.g., .env.test)
  4. .env.{NODE_ENV}.local - Local environment overrides
// Custom base path and environment
loadEnvFiles({
    basePath: "/app/config",
    nodeEnv: "staging",
});

Usage

Application Configuration Pattern

A typical pattern for hexai applications:

// config.ts
import { defineConfig, env, envBoolean, envNumber } from "@hexaijs/utils/config";
import { postgresConfig } from "@hexaijs/postgres";

export const getConfig = defineConfig(
    {
        db: postgresConfig("ORDER_DB"),  // ConfigSpec - errors collected properly
        port: envNumber("PORT"),
        debug: envBoolean("DEBUG", false),
        apiKey: env("API_KEY"),
    },
    { loadEnv: true }
);

// application-context.ts
import { getConfig } from "./config";

export class OrderApplicationContext {
    private readonly config = getConfig();

    getDatabase() {
        return this.config.db;
    }

    getPort() {
        return this.config.port;
    }
}

Testing with Environment Changes

In test environments, the config is recreated on each call:

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { defineConfig, env } from "@hexaijs/utils/config";

describe("MyService", () => {
    const originalEnv = process.env;

    beforeEach(() => {
        process.env = { ...originalEnv, NODE_ENV: "test" };
    });

    afterEach(() => {
        process.env = originalEnv;
    });

    it("uses API_KEY from environment", () => {
        process.env.API_KEY = "test-key";

        const getConfig = defineConfig({ apiKey: env("API_KEY") });

        expect(getConfig().apiKey).toBe("test-key");
    });

    it("reflects environment changes", () => {
        process.env.API_KEY = "first-key";
        const getConfig = defineConfig({ apiKey: env("API_KEY") });

        expect(getConfig().apiKey).toBe("first-key");

        process.env.API_KEY = "second-key";
        expect(getConfig().apiKey).toBe("second-key");
    });
});

API Highlights

| Export | Description | |--------|-------------| | defineConfig(schema, options?) | Creates typed config getter with validation | | env(key) | Required string environment variable | | envOptional(key, default?) | Optional string with default | | envNumber(key) | Required number (transforms string) | | envNumberOptional(key, default?) | Optional number | | envBoolean(key, default) | Boolean ("true", "1" → true) | | envJson<T>(key) | Required JSON-parsed value | | envJsonOptional<T>(key, default?) | Optional JSON value | | ConfigSpec<T> | Interface for custom config specs | | ConfigValidationError | Error containing all validation failures | | DatabaseConfig | Common interface for database configs | | loadEnvFiles(options?) | Loads .env files with NODE_ENV awareness |

See Also