Skip to content

Feature: ConfigModule & ConfigService (environment-aware configuration) #114

@ItayTheDar

Description

@ItayTheDar

Overview

PyNest has no first-party configuration abstraction. Developers currently read environment variables directly via os.getenv() scattered across services, or use python-dotenv manually with no validation. There is no typed, injectable configuration layer.

This feature request proposes a ConfigModule and ConfigService modeled after NestJS's @nestjs/config package, built on top of Pydantic BaseSettings.


Motivation

  • Config values are read in random places, making them untestable and hard to mock
  • No validation of required environment variables at startup (fail fast)
  • No namespaced config for feature modules
  • No support for different .env files per environment (dev, staging, prod)

Proposed API

ConfigModule.for_root()

```python
@module(
imports=[
ConfigModule.for_root(
env_file=".env",
is_global=True, # makes ConfigService available everywhere
validate=AppConfig, # Pydantic BaseSettings schema
ignore_env_file=False,
)
],
...
)
class AppModule:
pass
```

Validation schema via Pydantic BaseSettings

```python
from pydantic_settings import BaseSettings

class AppConfig(BaseSettings):
database_url: str
jwt_secret: str
redis_host: str = "localhost"
redis_port: int = 6379
debug: bool = False

class Config:
    env_file = ".env"

```

If database_url or jwt_secret are missing, PyNest raises a ConfigValidationException at startup — before any provider is instantiated.

ConfigService — injectable anywhere

```python
@Injectable
class UserService:
def init(self, config: ConfigService):
self.db_url = config.get("database_url") # str | None
self.db_url = config.get_or_throw("database_url") # str | raises
self.port = config.get("redis_port", default=6379)
```

Typed access with config.get_typed()

```python

Returns a fully validated Pydantic model instance

app_config: AppConfig = config.get_typed(AppConfig)
print(app_config.jwt_secret)
```

ConfigModule.for_feature() — namespaced module config

```python
@module(
imports=[
ConfigModule.for_feature("database", DatabaseConfig)
],
providers=[DatabaseService],
)
class DatabaseModule:
pass

In DatabaseService:

@Injectable
class DatabaseService:
def init(self, config: ConfigService):
db_config = config.get_typed("database") # returns DatabaseConfig
```


Environment File Precedence

  1. .env.{NODE_ENV}.local (e.g. .env.development.local)
  2. .env.local
  3. .env.{NODE_ENV} (e.g. .env.development)
  4. .env

Controlled by env_file and env_file_encoding options on for_root().


expandVariables Support

```

.env

BASE_URL=https://api.example.com
AUTH_URL=${BASE_URL}/auth
```

Set expand_variables=True in for_root() to resolve ${VAR} references.


Acceptance Criteria

  • ConfigModule with for_root(env_file, validate, is_global, ignore_env_file, expand_variables) factory method
  • ConfigModule.for_feature(namespace, schema) for module-scoped config
  • ConfigService injectable with get(), get_or_throw(), get_typed() methods
  • Pydantic BaseSettings integration for schema validation at startup
  • ConfigValidationException raised at boot if validation fails (fail fast)
  • Support for multiple env files with precedence order
  • is_global=True makes ConfigService injectable without re-importing ConfigModule
  • Env variable expansion support (expand_variables=True)
  • Full unit tests including missing-required-var scenarios
  • Works alongside existing python-dotenv optional dependency

Implementation Notes

  • pydantic-settings should be added as an optional dependency (pip install pynest-api[config])
  • ConfigModule.for_root() returns a DynamicModule — this is also a prerequisite for the Dynamic Modules pattern (a separate future feature)
  • ConfigService should be registered as a singleton scoped to the module graph
  • Validation should happen inside the OnModuleInit hook (once Feature Add official docs #2 / Lifecycle Hooks lands)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions