From 51afe79386ab7b3d523198c76d7f62c694a5576a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 03:10:27 +0000 Subject: [PATCH] content: Migrating from LaunchDarkly to OpenFeature in NestJS Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01X5ckmFZL59o8y12UdpTkDq --- ...unchdarkly-openfeature-nestjs-migration.md | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 src/content/docs/blog/launchdarkly-openfeature-nestjs-migration.md diff --git a/src/content/docs/blog/launchdarkly-openfeature-nestjs-migration.md b/src/content/docs/blog/launchdarkly-openfeature-nestjs-migration.md new file mode 100644 index 0000000..c3d91d5 --- /dev/null +++ b/src/content/docs/blog/launchdarkly-openfeature-nestjs-migration.md @@ -0,0 +1,242 @@ +--- +title: "Migrating from LaunchDarkly to OpenFeature in NestJS" +description: "Audit your NestJS app's LaunchDarkly flag debt and migrate to OpenFeature in six steps — real CLI commands, before/after diffs, and CI enforcement." +date: 2026-08-13 +authors: + - name: Krishan Sharma + title: Founder and maintainer of FlagLint + url: https://www.linkedin.com/in/krishansha/ +tags: ["launchdarkly", "openfeature", "nestjs", "migration", "nodejs"] +--- + +NestJS teams accumulate LaunchDarkly flag debt the same way everyone else does — flags ship behind `boolVariation` calls, the feature rolls out, and the flag stays. Multiply that by a service layer spread across a checkout module, a pricing module, and a promotions module, and a year later you have dozens of live flag keys, a handful of stale ones, and no clear picture of how long a migration plan would actually take. + +The specific challenge with OpenFeature NestJS migrations is that the LaunchDarkly SDK can live in three or four different services simultaneously. A grep across `./src` tells you how many call sites exist, but it conflates flag keys, call types, and staleness signals into one undifferentiated count — giving you no basis for estimating effort or scheduling the work. + +This article walks through a complete OpenFeature NestJS migration using FlagLint: audit your flag debt, understand what the tool can safely rewrite versus what needs manual review, set up the OpenFeature provider, and enforce the boundary in CI. + +--- + +## Step 1 — Audit your flag debt + +Start with a dry measurement of what you're working with. Run the audit against your service source, excluding tests so test fixtures don't inflate your counts: + +```bash +npx flaglint@latest audit ./src --exclude-tests +``` + +Real output from a three-module NestJS service: + +``` +- Auditing ./src... +# FlagLint Audit Report + +**Scanned at:** 2026-08-13T03:07:33.316Z +**Scan root:** ./src +**Files scanned:** 3 +**Duration:** 41ms + +## Summary + +| Total Flags | High Risk | Medium Risk | Total Usages | +|-------------|-----------|-------------|--------------| +| 7 | 2 | 5 | 7 | + +| Dynamic Keys | Detail Evals | Bulk Calls | Stale Signals | Safely Automatable | Manual Review | +|--------------|--------------|------------|---------------|-------------------|---------------| +| 0 | 1 | 0 | 1 | 6 | 1 | + +## Migration Readiness + +Migration readiness: **86/100** · ready + +[██████████████████████░░░] 86% + +6 safely automatable · 1 require manual review + +## Flag Debt Inventory + +| Flag Key | Risk | Usages | Files | Call Types | Reasons | +|----------|------|--------|-------|------------|---------| +| `promo-discount-amount` | 🔴 High | 1 | 1 | variationDetail | detail evaluation | +| `temp-discount-banner` | 🔴 High | 1 | 1 | boolVariation | stale signal | +| `new-checkout-flow` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable | +| `checkout-discount-rate` | 🟢 Automatable | 1 | 1 | numberVariation | safely automatable | +| `checkout-ui-theme` | 🟢 Automatable | 1 | 1 | stringVariation | safely automatable | +| `promo-codes-enabled` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable | +| `dynamic-pricing-v2` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable | + +## Next Steps + +- Run `flaglint migrate --dry-run` to preview safe OpenFeature rewrites +- Run `flaglint validate --no-direct-launchdarkly` to enforce OF boundary in CI +- Review HIGH risk flags manually before any automated migration + +✓ Audit complete: 7 flags — 2 high risk, 5 medium risk (41ms, 3 files) + +Migration readiness: 86/100 · ready +[██████████████████████░░░] 86% +6 safely automatable · 1 require manual review +``` + +The readiness score of 86/100 means the majority of this migration plan can be automated. Two flag keys block a clean score: + +- **`promo-discount-amount`** — call type is `variationDetail`, which returns a reason object alongside the value. OpenFeature has a detail API, but the response shape differs from LaunchDarkly's. This requires manual review. +- **`temp-discount-banner`** — the flag key contains a staleness signal (`temp`). FlagLint surfaces this as a warning, not a block: you should verify whether to remove the flag entirely or migrate it. + +The five remaining flag keys — boolean, number, and string calls with static flag keys — are safely automatable. + +--- + +## Step 2 — Set up the OpenFeature provider service + +Before applying any rewrites, wire up the OpenFeature provider in NestJS. The key point: you keep your LaunchDarkly SDK. It becomes the OpenFeature provider backend, so your flag keys, targeting rules, and rollout configurations are unchanged in the LaunchDarkly dashboard. + +Install the packages: + +```bash +npm install @openfeature/server-sdk @launchdarkly/openfeature-node-server +``` + +Create a shared `FeatureFlagsService` that your modules inject: + +```ts +// src/platform/feature-flags.service.ts +import { Injectable, OnModuleInit } from "@nestjs/common"; +import { OpenFeature, Client } from "@openfeature/server-sdk"; +import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server"; + +@Injectable() +export class FeatureFlagsService implements OnModuleInit { + client!: Client; + + async onModuleInit() { + await OpenFeature.setProviderAndWait( + new LaunchDarklyProvider(process.env.LD_SDK_KEY!) + ); + this.client = OpenFeature.getClient(); + } +} +``` + +Export this service from a shared `FeatureFlagsModule` and import that module wherever flag evaluation happens. Keeping the OpenFeature client in one place means you swap providers later — from LaunchDarkly to Flipt or another backend — without touching your service layer. + +Tell FlagLint about the binding so the `validate` command knows your team's OpenFeature client is `featureFlagsService.client`. Add this to `.flaglintrc`: + +```json +{ + "exclude": ["**/*.spec.ts", "**/*.test.ts"], + "openFeatureClientBindings": [ + { + "importName": "openFeatureClient", + "modulePatterns": ["**/platform/feature-flags.service"] + } + ] +} +``` + +--- + +## Step 3 — Preview the rewrites + +Run the migration in dry-run mode to see exactly what FlagLint will change before touching any file: + +```bash +npx flaglint@latest migrate ./src --dry-run --exclude-tests +``` + +``` +- Scanning ./src... +LaunchDarkly usages found: 7 +Safely automatable: 6 · Manual review: 1 + +Reviewable diffs: 6 +Skipped usages: 1 +``` + +The generated diffs for the six safely automatable flag keys: + +```diff +--- a/platform/ld-client.ts ++++ b/platform/ld-client.ts +@@ -7,1 +7,1 @@ +- const newCheckout = await ldClient.boolVariation("new-checkout-flow", ctx, false); ++ const newCheckout = await openFeatureClient.getBooleanValue("new-checkout-flow", false, ctx); +@@ -8,1 +8,1 @@ +- const discountRate = await ldClient.numberVariation("checkout-discount-rate", ctx, 0); ++ const discountRate = await openFeatureClient.getNumberValue("checkout-discount-rate", 0, ctx); +@@ -9,1 +8,1 @@ +- const checkoutTheme = await ldClient.stringVariation("checkout-ui-theme", ctx, "default"); ++ const checkoutTheme = await openFeatureClient.getStringValue("checkout-ui-theme", "default", ctx); +@@ -15,1 +15,1 @@ +- const promoEnabled = await ldClient.boolVariation("promo-codes-enabled", ctx, false); ++ const promoEnabled = await openFeatureClient.getBooleanValue("promo-codes-enabled", false, ctx); +@@ -23,1 +23,1 @@ +- const dynamicPricing = await ldClient.boolVariation("dynamic-pricing-v2", ctx, false); ++ const dynamicPricing = await openFeatureClient.getBooleanValue("dynamic-pricing-v2", false, ctx); +@@ -24,1 +24,1 @@ +- const discountFlag = await ldClient.boolVariation("temp-discount-banner", ctx, false); ++ const discountFlag = await openFeatureClient.getBooleanValue("temp-discount-banner", false, ctx); +``` + +Notice the argument order flip: LaunchDarkly SDK takes `(flagKey, context, fallback)` while OpenFeature takes `(flagKey, fallback, context)`. A regex-based migration misses this entirely — it produces syntactically valid code that silently evaluates the wrong flag state in production. FlagLint's AST scanner applies the correct transposition for each call type. See [why argument-order bugs are the most common migration mistake](/blog/launchdarkly-openfeature-argument-order-bug/). + +The skipped call: + +``` +platform/ld-client.ts:17:28 — `promo-discount-amount` via `variationDetail`: +detail methods skipped: OpenFeature detail APIs exist, but LaunchDarkly/OpenFeature +detail result parity requires manual review +``` + +For `variationDetail` and its typed variants, FlagLint marks the call as requiring manual review rather than generating a diff. The OpenFeature equivalent is `getBooleanDetails`/`getStringDetails`/etc., but the reason enum values in the response differ from LaunchDarkly's `EvaluationReason`. Review these manually before applying. See [five patterns that block automatic migration](/blog/five-patterns-that-block-migration/) for the complete list. + +--- + +## Step 4 — Apply the migration + +Once you've reviewed the dry-run output, apply it on a branch: + +```bash +git checkout -b migrate/openfeature +npx flaglint@latest migrate ./src --apply --exclude-tests +``` + +Replace the `openFeatureClient` placeholder in each rewritten call site with your `featureFlagsService.client` reference from Step 2. The placeholder is intentional: FlagLint knows the OpenFeature client variable name is project-specific and won't assume it. + +--- + +## Step 5 — Enforce the OpenFeature boundary in CI + +The migration is only as durable as your CI gate. Without enforcement, new LaunchDarkly SDK calls can creep back in as team members add features — eroding the OpenFeature NestJS boundary you just established. + +Add validation to your GitHub Actions workflow: + +```yaml +- name: Enforce OpenFeature boundary + run: npx flaglint@latest validate ./src --no-direct-launchdarkly --exclude-tests +``` + +The `validate` command exits non-zero if any call to the LaunchDarkly SDK's direct evaluation methods is found. Combined with the `openFeatureClientBindings` config from Step 2, FlagLint distinguishes between your shared OpenFeature provider setup (allowed) and new direct LaunchDarkly SDK calls (blocked). + +For a complete CI workflow including baseline mode and PR comments, see [enforcing your LaunchDarkly to OpenFeature migration in GitHub Actions](/blog/enforce-launchdarkly-migration-github-actions/). + +--- + +## Step 6 — Handle the two high-risk flags + +Return to the two flags from the audit that need manual decisions: + +**`promo-discount-amount` (variationDetail):** Check whether the caller uses the `reason` field from the detail response. If it does, implement the equivalent using `getBooleanDetails` or `getNumberDetails` and map the reason enum to your application's needs. If the reason field is unused, convert the call to a plain `getBooleanValue`/`getNumberValue` and remove the detail call. + +**`temp-discount-banner` (stale signal):** This flag key triggered a staleness signal because `temp` is a known staleness indicator. Check LaunchDarkly's dashboard: if the flag is fully rolled out to 100% of users, remove it from the codebase entirely rather than migrating it. Removing dead flag debt is cheaper than carrying it across the OpenFeature boundary. + +--- + +## Next steps + +The full step-by-step NestJS guide — including module wiring, wrapper detection configuration, and how to handle shared evaluation services — is at [/docs/guides/nestjs/](/docs/guides/nestjs/). + +For the underlying concepts behind how FlagLint classifies flag debt and generates readiness scores, see [how FlagLint works](/docs/concepts/how-flaglint-works/). + +If your team is evaluating the total migration effort before scheduling work, the [flag debt audit](/blog/launchdarkly-flag-debt/) article covers how to turn the audit output into an hour estimate you can take to your engineering manager.