Evaluate in your application
Node.js SDK
In Node.js, the JavaScript SDK runs once per process and serves every request from the same datafile. Each request brings its own context.
Install#
npm install @featurevisor/sdkImport it in an ES module:
import { createFeaturevisor } from '@featurevisor/sdk'In CommonJS, use const { createFeaturevisor } = require('@featurevisor/sdk'). The JavaScript SDK page is the reference for every method.
One instance per process#
Create one instance for each datafile the process uses, and share it across requests. Load its datafile before accepting requests, or start from a bundled copy. Every worker and serverless instance has its own memory, so each loads and refreshes its own.
Reusable refresh helper#
This helper fetches a datafile and replaces the instance's datafile with it. It is application code, not part of the SDK, and works anywhere fetch and AbortSignal.timeout do. Save it as datafile-refresh.mjs:
const pendingRefreshes = new WeakMap()function refreshDatafile(f, datafileUrl) { const pendingRefresh = pendingRefreshes.get(f) if (pendingRefresh) return pendingRefresh const refresh = (async () => { const response = await fetch(datafileUrl, { signal: AbortSignal.timeout(10000), }) if (!response.ok) throw new Error(`HTTP ${response.status}`) const datafile = await response.json() if (!f.setDatafile(datafile, true)) { throw new Error('Datafile was not accepted') } return datafile })().finally(() => { pendingRefreshes.delete(f) }) pendingRefreshes.set(f, refresh) return refresh}async function waitForRefresh(f) { await pendingRefreshes.get(f)?.catch(() => {})}export { refreshDatafile, waitForRefresh }- Calls made while a fetch is running share it, so one instance never fetches twice at once.
- Replacing the datafile removes definitions the new one no longer has.
- A failed fetch, or a datafile the SDK rejects, keeps the last good one. The reason for a rejection is in the diagnostics.
- Use one instance per datafile URL. From CommonJS, load the helper with
await import('./datafile-refresh.mjs').
Start and stop#
Await the first refresh before the server accepts requests, so it never runs without configuration. Then refresh every few minutes:
import { createFeaturevisor } from '@featurevisor/sdk'import { refreshDatafile, waitForRefresh,} from './datafile-refresh.mjs'const f = createFeaturevisor({})const datafileUrl = 'https://cdn.example.com/featurevisor-all.json'try { await refreshDatafile(f, datafileUrl)} catch (error) { await f.close() throw error}const refreshTimer = setInterval(() => { refreshDatafile(f, datafileUrl).catch((error) => console.error('Refresh failed:', error), )}, 5 * 60 * 1000)refreshTimer.unref()unref() lets the process exit without waiting for the timer. On shutdown, stop accepting requests and let the current ones finish, then stop refreshing and close:
clearInterval(refreshTimer)await waitForRefresh(f)await f.close()Express and Fastify show where this goes in their lifecycles. A serverless function can refresh when its copy is older than some limit instead of running a timer.
Context per request#
Build the context from the authenticated request, and pass it to each evaluation. Never call setContext() on the shared instance for one user, because every concurrent request would see it:
function showNewCheckout(request) { if (!request.user) return false const context = { userId: String(request.user.id), country: request.user.country, } return f.isEnabled('checkout', context)}When several functions in one request evaluate with the same context, spawn a child instance for it. A child sees the parent's datafile updates, but keeps its context to itself:
function checkoutPage(request) { const child = f.spawn({ userId: String(request.user.id), country: request.user.country, }) try { return { newCheckout: child.isEnabled('checkout'), buyLabel: child.getVariable('buyButton', 'label'), settings: child.getVariable('checkoutSettings'), } } finally { child.close() }}For user-4 in the Netherlands, this returns the new checkout, the label Nu kopen and settings in euros.
OpenFeature#
If your service uses OpenFeature, install the Node.js provider with the OpenFeature server SDK:
npm install \ @featurevisor/openfeature-provider-node \ @openfeature/server-sdkimport { OpenFeature } from '@openfeature/server-sdk'import { FeaturevisorOpenFeatureProvider,} from '@featurevisor/openfeature-provider-node'const provider = new FeaturevisorOpenFeatureProvider({ datafile,})await OpenFeature.setProviderAndWait(provider)const client = OpenFeature.getClient()const context = { targetingKey: 'user-4', country: 'nl',}const enabled = await client.getBooleanValue( 'checkout', false, context,)The targeting key becomes userId, so Featurevisor evaluates user-4 in the Netherlands and enabled is true.
If an invalid datafile is given at the start, setProviderAndWait rejects with PARSE_ERROR, whatever the log level. A valid datafile passed later through provider.featurevisor.setDatafile() makes the provider ready again.
Reuse an existing instance#
If your application already has an instance, pass it as featurevisor:
import { createFeaturevisor } from '@featurevisor/sdk'const featurevisor = createFeaturevisor({ datafile })const provider = new FeaturevisorOpenFeatureProvider({ featurevisor,})await OpenFeature.setProviderAndWait(provider)The provider borrows the instance, so your application still closes it once everything using it has finished. An instance you pass takes precedence over options such as datafile and modules. Options specific to the provider still apply.
The OpenFeature page covers key conventions, variables, variations and errors.
Examples#
- featurevisor-example-nodejs, a Node.js application
- featurevisor-example-openfeature-nodejs, the same through OpenFeature, with context mapping, defaults and the provider lifecycle

