Skip to content

nestjs

1 post with the tag “nestjs”

OpenFeature NestJS: Audit and Migrate Your LaunchDarkly Services With FlagLint

NestJS services accumulate LaunchDarkly SDK calls the same way every other Node.js codebase does — one ldClient.boolVariation() at a time. The difference is that in NestJS, the LaunchDarkly client is typically injected as a module dependency. That makes the debt feel more tangled than it is: you can’t grep your way to a count because the same injected ldClient appears across dozens of services, and you have no view of which calls are safe to automate versus which ones will require manual work.

This article walks through the full migration cycle for a NestJS application: measure the flag debt with FlagLint, wire the OpenFeature NestJS provider with Nest’s dependency injection system, auto-rewrite call sites, and lock the boundary in CI.

Before touching any code, get a complete picture of what you have. Run flaglint audit against your source directory, excluding test files so fixture data doesn’t inflate the count:

Terminal window
npx flaglint@latest audit ./src --exclude-tests

Real output from a three-service NestJS codebase with a checkout module, a pricing module, and a recommendations module:

- Auditing ./src...
# FlagLint Audit Report
**Scanned at:** 2026-08-06T03:04:42.342Z
**Files scanned:** 3
**Duration:** 51ms
## Summary
| Total Flags | High Risk | Medium Risk | Total Usages |
|-------------|-----------|-------------|--------------|
| 8 | 1 | 7 | 8 |
| Dynamic Keys | Detail Evals | Bulk Calls | Stale Signals | Safely Automatable | Manual Review |
|--------------|--------------|------------|---------------|-------------------|---------------|
| 1 | 0 | 0 | 0 | 7 | 1 |
## Migration Readiness
Migration readiness: **88/100** · ready
[██████████████████████░░░] 88%
7 safely automatable · 1 require manual review

The readiness score of 88 means the codebase is in good shape: 7 of 8 flag usages can be rewritten automatically. The single high-risk entry is a dynamic flag key — a call where the flag key is computed at runtime instead of being a string literal. That one requires manual attention before any automated migration runs. Everything else is automatable.

The stale signals column is zero here. FlagLint checks flag key names for keywords like old, deprecated, legacy, tmp, and test, and checks whether call sites live in deprecated directories. Zero means no obvious staleness signal at the source level; git-history-based staleness, which checks last-evaluation date against git metadata, is outside a static scan.

LaunchDarkly stays your feature flag backend — you are not removing it. You are replacing the LaunchDarkly SDK as your application’s evaluation interface with the OpenFeature standard API, and using the LaunchDarkly OpenFeature provider as the bridge.

The NestJS-native path uses @openfeature/nestjs-sdk, which provides a OpenFeatureModule that plugs into Nest’s DI system. Install the required packages:

Terminal window
npm install @openfeature/server-sdk @openfeature/nestjs-sdk \
@launchdarkly/node-server-sdk @launchdarkly/openfeature-node-server

Register the OpenFeature module once in your root AppModule:

import { Module } from "@nestjs/common";
import { OpenFeatureModule } from "@openfeature/nestjs-sdk";
import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
@Module({
imports: [
OpenFeatureModule.forRoot({
provider: new LaunchDarklyProvider(process.env.LD_SDK_KEY!),
}),
],
})
export class AppModule {}

forRoot calls OpenFeature.setProviderAndWait internally, so the provider is ready before the application starts accepting requests. With the module registered, inject the OpenFeature client into any service:

import { Injectable } from "@nestjs/common";
import { InjectFeatureClient } from "@openfeature/nestjs-sdk";
import { Client } from "@openfeature/server-sdk";
@Injectable()
export class CheckoutService {
constructor(
@InjectFeatureClient() private readonly featureClient: Client
) {}
async isExpressCheckoutEnabled(userId: string): Promise<boolean> {
const ctx = { targetingKey: userId };
return this.featureClient.getBooleanValue("express-checkout", false, ctx);
}
}

This is the target state. FlagLint gets you from the ldClient.boolVariation(...) call sites to the featureClient.getBooleanValue(...) call sites automatically on the files where it can prove the OpenFeature client binding.

Before applying any rewrites, tell FlagLint where the shared OpenFeature client lives so it can resolve the binding at call sites that import it. Create .flaglintrc in your project root:

{
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
"openFeatureClientBindings": [
{
"importName": "openFeatureClient",
"modulePatterns": ["**/platform/feature-flags"]
}
]
}

Then run the dry-run preview:

Terminal window
npx flaglint@latest migrate ./src --dry-run --exclude-tests

Real output for the same three-service codebase:

- Scanning ./src...
LaunchDarkly usages found: 8
Safely automatable: 7 · Manual review: 1
Reviewable diffs: 7
Diffs requiring provider setup: 1
Skipped usages: 1

The diff output shows exactly what FlagLint will change:

diff --git a/checkout.service.ts b/checkout.service.ts
--- a/checkout.service.ts
+++ b/checkout.service.ts
@@ -8,1 +8,1 @@
- return ldClient.boolVariation("express-checkout", ctx, false);
+ return openFeatureClient.getBooleanValue("express-checkout", false, ctx);
@@ -13,1 +13,1 @@
- return ldClient.numberVariation("max-cart-items", ctx, 20);
+ return openFeatureClient.getNumberValue("max-cart-items", 20, ctx);
@@ -18,1 +18,1 @@
- return ldClient.stringVariation("checkout-theme", ctx, "default");
+ return openFeatureClient.getStringValue("checkout-theme", "default", ctx);
diff --git a/pricing.service.ts b/pricing.service.ts
--- a/pricing.service.ts
+++ b/pricing.service.ts
@@ -8,1 +8,1 @@
- return ldClient.boolVariation("dynamic-pricing", ctx, false);
+ return openFeatureClient.getBooleanValue("dynamic-pricing", false, ctx);
@@ -13,1 +13,1 @@
- return ldClient.stringVariation("pricing-tier", ctx, "standard");
+ return openFeatureClient.getStringValue("pricing-tier", "standard", ctx);
@@ -18,1 +18,1 @@
- return ldClient.numberVariation("discount-basis-points", ctx, 0);
+ return openFeatureClient.getNumberValue("discount-basis-points", 0, ctx);

Notice the argument order swap. Every OpenFeature rewrite places the fallback value at position two and the evaluation context at position three — the reverse of the LaunchDarkly SDK convention. This is the argument-order difference that silently breaks flag evaluations in production when teams do this migration by hand. FlagLint inverts the arguments correctly on every rewritten call site; it is also the most common source of production bugs in hand-rolled migrations. Review the full dry-run output before applying.

The dry-run skipped one call site:

recommendations.service.ts:13:9 — `flagKey` via `stringVariation`: dynamic key requires manual review

The flag key in recommendations.service.ts is computed at runtime:

const flagKey = "recommendation-model-" + userId.substring(0, 3);
return ldClient.stringVariation(flagKey, ctx, "collaborative");

FlagLint cannot statically determine which flag key is being evaluated, so it cannot generate a safe rewrite. The resolution here is to extract the key logic into an explicit lookup table or enumerate the specific flag keys this code path evaluates. Once the key is a static literal, a subsequent flaglint audit run will classify it as automatable.

After reviewing the dry-run output and resolving any manual-review call sites, apply the proven rewrites:

Terminal window
git checkout -b migrate/openfeature
npx flaglint@latest migrate ./src --apply --exclude-tests

FlagLint writes the diffs directly to source files. Run your test suite. Each rewritten call site evaluates identically at runtime because LaunchDarkly remains the provider — the only change is which API your application code calls.

Once the migration is applied and merged, add a validate step to CI that blocks any new LaunchDarkly SDK call from entering the codebase. This is the gate that prevents flag debt from re-accumulating:

Terminal window
npx flaglint@latest validate ./src --no-direct-launchdarkly --exclude-tests

Exit code is 0 when no direct LaunchDarkly evaluation calls are found:

✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found.
Scanned 3 file(s).

Exit code is 1 if any direct call remains, which fails the CI job. Add this to your GitHub Actions workflow after tests pass — the enforce in GitHub Actions tutorial has a ready-made workflow file.

From this point, OpenFeature NestJS is the only path for feature flag evaluation in your application. Switching providers in the future — from LaunchDarkly to Unleash, Flagd, or any OpenFeature-compliant backend — requires one configuration change in AppModule. No call sites change.