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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions