diff --git a/src/content/docs/blog/openfeature-nestjs-launchdarkly-migration.md b/src/content/docs/blog/openfeature-nestjs-launchdarkly-migration.md new file mode 100644 index 0000000..aa5785e --- /dev/null +++ b/src/content/docs/blog/openfeature-nestjs-launchdarkly-migration.md @@ -0,0 +1,226 @@ +--- +title: "OpenFeature NestJS: Audit and Migrate Your LaunchDarkly Services With FlagLint" +description: "Audit LaunchDarkly flag debt in NestJS with FlagLint, wire the OpenFeature NestJS module via DI, auto-rewrite call sites, and enforce the boundary in CI." +date: 2026-08-06 +authors: + - name: Krishan Sharma + title: Founder and maintainer of FlagLint + url: https://www.linkedin.com/in/krishansha/ +tags: ["launchdarkly", "openfeature", "nestjs", "migration", "nodejs"] +--- + +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. + +## Measure the flag debt first + +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: + +```bash +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. + +## Wire the OpenFeature NestJS provider + +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: + +```bash +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`: + +```ts +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: + +```ts +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 { + 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. + +## Preview the migration with a dry run + +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: + +```json +{ + "exclude": ["**/*.spec.ts", "**/*.test.ts"], + "openFeatureClientBindings": [ + { + "importName": "openFeatureClient", + "modulePatterns": ["**/platform/feature-flags"] + } + ] +} +``` + +Then run the dry-run preview: + +```bash +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 +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. + +## Handle the dynamic flag key manually + +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: + +```ts +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. + +## Apply the migration on a branch + +After reviewing the dry-run output and resolving any manual-review call sites, apply the proven rewrites: + +```bash +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. + +## Enforce the OpenFeature boundary in CI + +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: + +```bash +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](/docs/tutorials/enforce-in-github-actions/) 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. + +## Next steps + +- [NestJS guide](/docs/guides/nestjs/) — detailed reference for the full audit-to-enforce cycle with NestJS-specific patterns +- [LaunchDarkly to OpenFeature Node.js migration guide](/docs/guides/launchdarkly-to-openfeature-nodejs/) — end-to-end migration workflow with provider setup +- [Manual Review Patterns](/docs/guides/manual-review-patterns/) — how to resolve dynamic flag keys and detail evaluations before running migrate +- [Migration Readiness](/docs/concepts/migration-readiness/) — how the readiness score is calculated and what each grade means +- [`flaglint audit` CLI reference](/docs/cli/audit/) — all options and output formats