Express.js - Featurevisor
Featurevisor

Framework guides

Express.js

An Express.js process keeps one Featurevisor instance for every request, and each request evaluates with its own context.

Install

In an existing Express application:

Command
npm install @featurevisor/sdk

Save the refresh helper from the Node.js guide as datafile-refresh.mjs, next to server.mjs. You need only the helper, not the Node.js startup code.

The server

This server loads the datafile before it listens, refreshes it every five minutes, and closes cleanly on SIGTERM. It expects authentication middleware that sets req.user from a verified session:

server.mjs
import express from 'express'
import { createServer } from 'node:http'
import { once } from 'node:events'
import { createFeaturevisor } from '@featurevisor/sdk'
import { refreshDatafile, waitForRefresh } from './datafile-refresh.mjs'
const PORT = 3000
const DATAFILE_URL = 'https://cdn.example.com/featurevisor-all.json'
const app = express()
const server = createServer(app)
const f = createFeaturevisor({})
// Register your authentication middleware here, before the routes
app.get('/checkout', (req, res) => {
if (!req.user) return res.sendStatus(401)
const context = {
userId: String(req.user.id),
country: req.user.country,
}
if (f.isEnabled('checkout', context)) {
res.send('New checkout')
} else {
res.send('Checkout')
}
})
let refreshTimer
server.on('close', () => {
clearInterval(refreshTimer)
waitForRefresh(f).then(() => f.close()).catch(reportError)
})
async function start() {
await refreshDatafile(f, DATAFILE_URL)
server.listen(PORT)
await once(server, 'listening')
refreshTimer = setInterval(() => {
refreshDatafile(f, DATAFILE_URL).catch((error) =>
console.error('Refresh failed:', error),
)
}, 5 * 60 * 1000)
refreshTimer.unref()
const shutdown = () => {
clearInterval(refreshTimer)
// close runs once active requests have finished
server.close()
}
process.once('SIGTERM', shutdown)
process.once('SIGINT', shutdown)
}
function reportError(error) {
console.error('Application failed:', error)
process.exitCode = 1
}
start()
.catch(async (error) => {
reportError(error)
await f.close()
})
.catch(reportError)

Run it with node server.mjs. It needs a Node.js version with fetch and AbortSignal.timeout.

  • If the first datafile cannot be loaded, the server does not start.
  • A failed refresh keeps the previous datafile.
  • On shutdown, the server stops accepting requests, lets the current ones finish, waits for any refresh in progress, then closes the instance.
  • If your application already handles signals, add these steps to its handler instead. Never add a second refresh loop, and never call setContext() on the shared instance for one user.

A child instance per request

When routes in several modules evaluate for the same user, give each request a child instance with that user's context. Register this middleware after authentication and before the routes that use it, since Express runs middleware in the order it is added:

server.mjs
app.use((req, res, next) => {
if (req.user) {
req.features = f.spawn({
userId: String(req.user.id),
country: req.user.country,
})
res.on('close', () => req.features.close())
}
next()
})

A route then reads values without building the context again:

app.get('/basket', (req, res) => {
if (!req.features) return res.sendStatus(401)
res.json({
newCheckout: req.features.isEnabled('checkout'),
buyLabel: req.features.getVariable('buyButton', 'label'),
})
})

For user-4 in the Netherlands, this returns newCheckout: true and buyLabel: 'Nu kopen'. The child follows the parent's datafile updates, and res.on('close') releases it however the request ends.

TypeScript

Declare the property on Express's Request, in a custom.d.ts listed in tsconfig.json:

custom.d.ts
import type { FeaturevisorChildInstance } from '@featurevisor/sdk'
declare global {
namespace Express {
interface Request {
features?: FeaturevisorChildInstance
}
}
}

Your authentication library declares user the same way.

Previous
Astro