Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions src/content/docs/blog/openfeature-nestjs-launchdarkly-migration.md
Original file line number Diff line number Diff line change
@@ -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<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.

## 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
Loading