🔐 NestJS AWS Parameter Store Config
Seamless NestJS integration with AWS Systems Manager Parameter Store — one canonical SSM contract to rule all your configuration.
💡 Highlights
- 🏗️ Canonical SSM Contract — Enforces a deterministic ///// model, eliminating ad-hoc parameter naming chaos across teams
- ⚡ Single Recursive Load — Fetches all parameters under // at startup into an in-memory cache, serving lookups instantly with zero runtime SSM calls
- 🛡️ Fail-Fast Validation — Catches missing segments, empty strings, whitespace, and illegal '/' characters at boot time with explicit error messages
- 🔌 NestJS 12 and ES Modules — Built for NestJS 12 as an ES module package that CommonJS applications load through Node.js
require(esm), with register() and registerAsync() patterns including factory, class, and existing provider injection
📚 Table of Contents
- Description
- Tech Stack
- Features
- Architecture
- Project Structure
- Prerequisites
- Installation
- Usage
- Migration Notes
- Roadmap
- FAQ
- License
- Acknowledgments
📖 Description
Managing application configuration across multiple environments, namespaces, and deployment targets is one of the most error-prone aspects of cloud-native development. NestJS AWS Parameter Store Config eliminates that complexity by enforcing a single, canonical SSM path contract:
/<application>/<environment>/<namespace>/<instanceName>/<path...>
This module provides a first-class NestJS dynamic module that loads all parameters at startup into an in-memory cache, then exposes a type-safe, validated lookup API that resolves paths deterministically.
Real-World Use Cases
- Microservices on ECS/EKS: Each service registers with its own
namespaceandinstanceName, while sharing infrastructure parameters (database hosts, secret IDs) across namespaces. - Multi-Environment Deployments: Seamlessly switch between
local,development,staging, andproductionwithout changing a single line of application code. - Shared Infrastructure Metadata: A single recursive load from
/<app>/<env>caches everything — RDS connection strings, Secrets Manager references, CloudWatch config — accessible via simple path lookups. - Monorepo / Multi-Service Architectures: Different services can override
namespaceandinstanceNameper-call, accessing any parameter in the hierarchy without re-registering the module.
With built-in enums for 90+ namespace tokens (AWS, GCP, Azure, databases, message brokers, and more), strict validation that fails fast on misconfigured paths, and support for both synchronous and async module registration, this library turns AWS Parameter Store into a structured, predictable configuration backbone for your NestJS applications.
🛠️ Tech Stack
| Category | Technologies |
|---|---|
| Language | TypeScript |
| Runtime | Node.js |
| Framework | NestJS |
| Cloud | AWS Systems Manager (SSM) |
| Build Tool | Rollup, TypeScript Compiler |
| Linting | ESLint, Prettier |
| CI/CD | GitHub Actions, Semantic Release |
| Package Manager | npm |
| Code Quality | Husky, Commitlint, Lint-Staged |
🚀 Features
- ✨ Canonical Path Contract — All parameter lookups resolve through a strict
/<application>/<environment>/<namespace>/<instanceName>/<path>model, ensuring consistency across all services and environments - ✨ Recursive Parameter Loading — Loads all parameters under
/<application>/<environment>at startup using paginated SSM API calls, building an in-memory cache for instant lookups - ✨ Sync & Async Module Registration — Supports
register()for static config andregisterAsync()withuseFactory,useClass, anduseExistingpatterns for dynamic configuration - ✨ Per-Call Overrides — Override
namespace,instanceName,application, orenvironmenton any individualget()call while keeping module-level defaults - ✨ 90+ Built-In Namespace Tokens — Curated
ENamespaceenum covering AWS, GCP, Azure services, databases, message brokers, observability tools, and more - ✨ Lifecycle Environment Enum —
EEnvironmentprovidesLOCAL,DEVELOPMENT,STAGING,PRODUCTION, andTESTtokens out of the box - ✨ Strict Input Validation — Fails fast with descriptive errors for missing segments, empty strings, whitespace-only values, and segments containing
/ - ✨ ES Module Package — Ships one ES module build with source maps and TypeScript declarations whose relative imports carry
.jsextensions, sobundler,nodenextand CommonJSnode10consumers all typecheck - ✨ Secure Parameter Decryption — Optional
shouldDecryptParametersflag enables transparent decryption of SecureString parameters - ✨ Verbose Logging Mode — Optional
isVerboseflag enables detailed NestJS Logger output during parameter loading for debugging - ✨ Global Module — Registered as a
@Global()module, makingParameterStoreConfigServiceinjectable anywhere without re-importing - ✨ Zero Runtime Dependencies — Peer dependencies only —
@aws-sdk/client-ssmand@nestjs/commonare provided by your application
🏗 Architecture
System Architecture
flowchart TD
appModule[AppModule] -->|imports| configModule[ParameterStoreConfigModule]
configModule -->|provides| propertiesProvider[PropertiesProvider]
configModule -->|provides| ssmProvider[SSMClientProvider]
configModule -->|provides| parametersProvider[ParametersProvider]
configModule -->|provides| parameterStoreService[ParameterStoreService]
configModule -->|exports| configService[ParameterStoreConfigService]
ssmProvider -->|creates| ssmClient[SSMClient]
parameterStoreService -->|uses| ssmClient
parametersProvider -->|calls| parameterStoreService
configService -->|reads| propertiesProvider
configService -->|reads| parametersProvider
anyService[Any Injectable Service] -->|injects| configService
Data Flow
sequenceDiagram
participant App as NestJS App
participant Module as ConfigModule
participant SSMProvider as SSMClientProvider
participant ParamProvider as ParametersProvider
participant PSService as ParameterStoreService
participant AWS as AWS SSM API
participant Service as Your Service
participant ConfigSvc as ConfigService
App->>Module: register() / registerAsync()
Module->>SSMProvider: Create SSMClient with config
SSMProvider-->>Module: SSMClient instance
Module->>ParamProvider: Initialize parameter loading
ParamProvider->>PSService: getParametersByPath(/app/env)
loop Paginated Loading
PSService->>AWS: GetParametersByPathCommand
AWS-->>PSService: Parameters + NextToken
end
PSService-->>ParamProvider: All Parameter[] cached
ParamProvider-->>Module: In-memory parameter cache ready
Service->>ConfigSvc: get({ path, namespace?, ... })
ConfigSvc->>ConfigSvc: buildLookupPath() + validate
ConfigSvc->>ConfigSvc: Find in cached parameters
ConfigSvc-->>Service: string value or null
📁 Project Structure
Click to expand
NestJS-AWS-Parameter-Store-Config/
├── .github/
│ ├── workflows/
│ │ ├── mirror-to-codecommit.yml
│ │ ├── qodana-quality-scan.yml
│ │ ├── release.yml
│ │ ├── snyk-security-scan.yml
│ │ └── test.yml
│ └── dependabot.yml
├── scripts/
│ ├── test/
│ │ └── consumer-smoke.mjs
│ └── check-declaration-imports.cjs
├── src/
│ ├── modules/
│ │ ├── aws/
│ │ └── config/
│ ├── shared/
│ │ ├── constant/
│ │ ├── enum/
│ │ ├── interface/
│ │ └── provider/
│ └── index.ts
├── test/
│ ├── consumer/
│ └── unit/
├── CHANGELOG.md
├── commitlint.config.js
├── eslint.config.js
├── LICENSE
├── lint-staged.config.js
├── nest-cli.json
├── package.json
├── prettier.config.js
├── README.md
├── release.config.js
├── rollup.config.js
├── tsconfig.build.json
├── tsconfig.json
└── vitest.unit.config.js
📋 Prerequisites
- Node.js
^22.13.0 || >=24.11.0 @nestjs/common>=12.3.2 <13@aws-sdk/client-ssm>=3.535.0 <4- AWS credentials configured (IAM role, environment variables, or AWS CLI profile)
- AWS SSM parameters created following the canonical path convention
🛠 Installation
# Install the package and its required peer dependencies
npm install @elsikora/nestjs-aws-parameter-store-config @aws-sdk/client-ssm @nestjs/common
# Or with Yarn
yarn add @elsikora/nestjs-aws-parameter-store-config @aws-sdk/client-ssm @nestjs/common
# Or with pnpm
pnpm add @elsikora/nestjs-aws-parameter-store-config @aws-sdk/client-ssm @nestjs/common
### Building from Source
# Clone the repository
git clone https://github.com/ElsiKora/NestJS-AWS-Parameter-Store-Config.git
cd NestJS-AWS-Parameter-Store-Config
# Install dependencies
npm install
# Build the ES module output and check its imports
npm run build
# Run linting
npm run lint:all
# Type check
npm run lint:types
# Run unit tests
npm run test:unit
# Pack the build and run it in ESM (bundler, nodenext) and CommonJS (node10, default resolution) consumer apps
npm run test:consumer
💡 Usage
Quick Start
1. Register the Module
Add ParameterStoreConfigModule to your root AppModule with your canonical defaults:
import { Module } from "@nestjs/common";
import { EEnvironment, ENamespace, ParameterStoreConfigModule } from "@elsikora/nestjs-aws-parameter-store-config";
@Module({
imports: [
ParameterStoreConfigModule.register({
application: "gameport",
environment: EEnvironment.STAGING,
namespace: ENamespace.AWS_ECS_FARGATE,
instanceName: "reaper-api",
shouldDecryptParameters: true,
config: {
region: "eu-north-1",
},
}),
],
})
export class AppModule {}
2. Async Registration with ConfigService
For dynamic configuration that depends on environment variables or other providers:
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { EEnvironment, ENamespace, ParameterStoreConfigModule } from "@elsikora/nestjs-aws-parameter-store-config";
@Module({
imports: [
ConfigModule.forRoot(),
ParameterStoreConfigModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
application: configService.getOrThrow("APP_NAME"),
environment: configService.getOrThrow("NODE_ENV"),
namespace: ENamespace.AWS_ECS_FARGATE,
instanceName: configService.getOrThrow("SERVICE_NAME"),
shouldDecryptParameters: true,
shouldUseRecursiveLoading: true,
isVerbose: configService.get("VERBOSE") === "true",
config: {
region: configService.getOrThrow("AWS_REGION"),
},
}),
}),
],
})
export class AppModule {}
3. Inject and Use the Service
Since the module is @Global(), you can inject ParameterStoreConfigService in any provider:
import { Injectable } from "@nestjs/common";
import { ENamespace, ParameterStoreConfigService } from "@elsikora/nestjs-aws-parameter-store-config";
@Injectable()
export class DatabaseService {
constructor(private readonly config: ParameterStoreConfigService) {}
getDatabaseHost(): string {
// Resolves: /gameport/staging/aws-rds/aurora-postgres/host
return (
this.config.get({
namespace: ENamespace.AWS_RDS,
instanceName: "aurora-postgres",
path: ["host"],
}) ?? "localhost"
);
}
getDatabasePort(): number {
// Resolves: /gameport/staging/aws-rds/aurora-postgres/port
const port = this.config.get({
namespace: ENamespace.AWS_RDS,
instanceName: "aurora-postgres",
path: ["port"],
});
return port ? parseInt(port, 10) : 5432;
}
}
4. Cross-Namespace Lookups
Access parameters from different namespaces without re-registering:
@Injectable()
export class AppConfigService {
constructor(private readonly config: ParameterStoreConfigService) {}
// Uses module defaults: /gameport/staging/aws-ecs-fargate/reaper-api/api/version
getApiVersion(): string | null {
return this.config.get({ path: ["api", "version"] });
}
// Override namespace: /gameport/staging/aws-secrets-manager/database/secret-id
getDatabaseSecretId(): string | null {
return this.config.get({
namespace: ENamespace.AWS_SECRETS_MANAGER,
instanceName: "database",
path: ["secret-id"],
});
}
// Override everything: /other-app/production/aws-lambda/processor/timeout
getExternalConfig(): string | null {
return this.config.get({
application: "other-app",
environment: "production",
namespace: ENamespace.AWS_LAMBDA,
instanceName: "processor",
path: ["timeout"],
});
}
}
5. Custom Namespace Strings
Not limited to the built-in enums — pass any string:
this.config.get({
namespace: "custom-service",
instanceName: "my-instance",
path: ["some", "deep", "config"],
});
// Resolves: /gameport/staging/custom-service/my-instance/some/deep/config
Configuration Options Reference
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
application | string | ✅ | — | Product/system name (first path segment) |
environment | EEnvironment | string | ✅ | — | Lifecycle environment token |
namespace | ENamespace | string | ✅ | — | Default owner/runtime/resource token |
instanceName | string | ✅ | — | Default deployable unit or resource instance |
config | SSMClientConfig | ❌ | {} | AWS SSM client configuration |
shouldDecryptParameters | boolean | ❌ | false | Decrypt SecureString parameters |
shouldUseRecursiveLoading | boolean | ❌ | true | Recursively load all nested parameters |
isVerbose | boolean | ❌ | false | Enable verbose logging during parameter loading |
🔄 Migration Notes
Migrating to 3.0
3.0.0 moves the package to NestJS 12 and ships it as ES modules only. The configuration API, the canonical path contract and the exported names do not change.
| Requirement | 2.x | 3.0 |
|---|---|---|
| Node.js | 16 and later | ^22.13.0 || >=24.11.0 |
@nestjs/common | ^8 || ^9 || ^10 || ^11 | >=12.3.2 <13 |
| Module format | ESM and CommonJS | ESM only |
- Upgrade Node.js and NestJS first, following the NestJS migration guide. Applications on NestJS 11 or older, or on Node.js 20, stay on 2.x.
- Install peers with explicit ranges, for example
npm install @nestjs/common@^12 @aws-sdk/client-ssm@^3, and check the tree withnpm ls. - Import only from the package root. The 2.x
exportsmap already had only., so Node.js never resolved deep imports. 3.0 also removesdist/esm,dist/cjsand therequirecondition, so TypeScript withnode10resolution, which ignoresexports, no longer finds types for deep paths such as@elsikora/nestjs-aws-parameter-store-config/dist/cjs/...either.
ES module applications import the package as before, with moduleResolution set to bundler, node16 or nodenext. The declarations now carry .js extensions on every relative import, so nodenext consumers get the types.
CommonJS applications keep require("@elsikora/nestjs-aws-parameter-store-config"). Node.js loads the ES module through require(esm), which works without a flag on every supported Node.js release. TypeScript with module: commonjs finds the types either way: with moduleResolution: node10 through the top-level types field (TypeScript 6 then needs "ignoreDeprecations": "6.0"), and without moduleResolution, which TypeScript 6 resolves as bundler, through exports.
Importing the package has no side effects: it loads no .env file and writes nothing to the console. Logging happens only while parameters load and only with isVerbose: true. CI checks this on the packed tarball.
🛣 Roadmap
Click to expand
| Task / Feature | Status |
|---|---|
Canonical SSM path contract (/<app>/<env>/<ns>/<instance>/<path>) | ✅ Done |
| Sync and async module registration | ✅ Done |
| Paginated recursive parameter loading | ✅ Done |
| Per-call namespace and instance overrides | ✅ Done |
| Strict input validation with fail-fast errors | ✅ Done |
| ES module output with extension-qualified declarations | ✅ Done |
| NestJS 12 support | ✅ Done |
| Built-in ENamespace enum (90+ tokens) | ✅ Done |
| Verbose logging mode | ✅ Done |
| Automated semantic releases with backmerge | ✅ Done |
| TTL-based parameter cache refresh | 🚧 In Progress |
| Parameter change event subscriptions | 🚧 In Progress |
Batch getMany() API for multi-path lookups | 🚧 In Progress |
| Optional Zod/class-validator schema validation on retrieved values | 🚧 In Progress |
| Unit test suite and packed-tarball consumer smoke tests | ✅ Done |
❓ FAQ
Click to expand
❓ Frequently Asked Questions
Q: Does this module make SSM API calls on every get() invocation?
A: No. All parameters are loaded once at module initialization (startup) into an in-memory cache. The get() method performs a simple array lookup against the cached parameters — there are zero runtime SSM API calls.
Q: How does the module handle large numbers of parameters?
A: The ParameterStoreService automatically handles SSM API pagination. It continues fetching pages using NextToken until all parameters under /<application>/<environment> are loaded.
Q: Can I use custom strings instead of the built-in enums?
A: Yes! Both environment and namespace accept string types in addition to EEnvironment and ENamespace enums. This allows you to use custom tokens like "my-custom-namespace" when the built-in options don't fit.
Q: Which NestJS versions are supported?
A: 3.x supports NestJS 12 (@nestjs/common >=12.3.2 <13). For NestJS 8 to 11, use 2.x.
Q: Can I use the package from a CommonJS application?
A: Yes. The package is an ES module, and Node.js ^22.13.0 || >=24.11.0 loads it from require() through require(esm). TypeScript finds the types through the top-level types field with node10 resolution and through exports with node16, nodenext or bundler, which is also what TypeScript 6 uses for module: commonjs without moduleResolution. See Migration Notes.
Q: Do I need to import the module in every feature module?
A: No. ParameterStoreConfigModule is decorated with @Global(), so once registered in your root AppModule, the ParameterStoreConfigService is injectable in any provider across your application.
Q: What happens if a required segment (application, environment, etc.) is missing?
A: The module throws a descriptive error immediately at startup or at lookup time. It validates that no segment is undefined, null, empty, whitespace-only, or contains / characters.
Q: How do I decrypt SecureString parameters?
A: Set shouldDecryptParameters: true in your module registration options. This passes WithDecryption: true to the SSM GetParametersByPathCommand.
Q: Can I access parameters from a different application or environment?
A: Yes, but with a caveat. The get() method accepts per-call overrides for application and environment, but the in-memory cache only contains parameters loaded from the module's configured /<application>/<environment> prefix. To access parameters from a different prefix, you'd need to register a second module instance or ensure those parameters exist under the loaded prefix.
🔒 License
This project is licensed under MIT.
🙏 Acknowledgments
- NestJS — The progressive Node.js framework that makes building server-side applications a joy
- AWS SDK for JavaScript v3 — The modular AWS SDK powering the SSM client
- AWS Systems Manager Parameter Store — The secure, hierarchical configuration storage service
- Semantic Release — Fully automated version management and package publishing
- Rollup — The module bundler producing the ES module output
- ElsiKora — The team behind this module and the broader tooling ecosystem
Built with ❤️ for the NestJS community