Skip to main content

Express (@ops-ai/toggly-express)

Examples and requirements​

Open the Express sample. Read the README, then src/catalog.js for flag inputs, src/app.js for request-scoped middleware and Order checks, and test/http.test.js for gate and isolation expectations. The catalog lists current sample availability.

Identity, groups, and claims are targeting inputs, not authentication. Populate them from your trusted session; a caller-controlled identity header does not authorize a user.

Requires Node.js 18+ and Express 4 or 5 (express >=4.0.0). The compatibility suite retains Express 4.22.2 and covers current Express 5.2.1 with packed ESM and CommonJS consumers, typechecking, request isolation, gates, refresh, signed-definition rejection, and shutdown. Node 18/20/22 keep unit coverage; Node 24 and 26 run the packed-host checks.

Install​

npm install @ops-ai/toggly-express

Middleware​

import express from 'express'
import { togglyMiddleware, featureGate, closeExpressToggly } from '@ops-ai/toggly-express'

const app = express()

app.use(
togglyMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
verifySignatures: true,
enableStreaming: true,
}),
)

app.get('/dashboard', async (req, res) => {
if (await req.toggly!.isFeatureOn('new-dashboard')) {
return res.json({ version: 'v2' })
}
res.json({ version: 'v1' })
})

process.on('SIGTERM', () => closeExpressToggly())

req.toggly exposes client, features, identity, context, isFeatureOn, isFeatureOff, and evaluateFeatureGate.

Configure identity, groups, and claims once on middleware. HTTP segment fields (request.userAgent / acceptLanguage / country) are filled from headers via fromHttpRequest. Then req.toggly.isFeatureOn('X') needs no context argument — same pattern as .NET AddTogglyWeb.

app.use(
togglyMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
getIdentity: (req) => req.user?.id,
getGroups: (req) => req.user?.roles ?? [],
getClaims: (req) => ({ role: req.user?.role ?? '', plan: req.user?.plan ?? '' }),
// Or getContext: (req) => ({ identity, groups, claims, request })
}),
)

app.get('/checkout', async (req, res) => {
// Ambient identity / groups / claims / request already bound
if (await req.toggly!.isFeatureOn('MobileCheckout')) {
return res.json({ flow: 'mobile' })
}
res.json({ flow: 'desktop' })
})

Default identity (when getIdentity is unset): x-toggly-identity header, then req.session.userId if present.

When you supply getContext, returned fields are used; missing request keys are still filled from headers. Prefer getIdentity / getGroups / getClaims unless you need a full custom context.

Overrides and entity checks​

In the adapter, the request helper isFeatureOn(key) accepts only the flag key and uses the middleware's context. Passing a second argument does not override that context. Call its core client explicitly and merge the fields you want to preserve:

const toggly = req.toggly!
const evaluation = {
...toggly.context,
identity: 'impersonated-user',
claims: { ...toggly.context.claims, role: 'admin' },
}
await toggly.client.isFeatureOn('MobileCheckout', evaluation)

// A ready entity context keeps Order data separate from user targeting.
await toggly.client.isFeatureOn('OrderBadge', toggly.context, {
kind: 'Order',
key: 'ORD-42',
attributes: { Status: 'Paid' },
})

The entity kind/property must match the feature definition. See Node entity context for mapper registration and HTTP segment filters for request fields.

Feature gates​

app.get(
'/beta',
featureGate({ featureKey: 'beta-access' }),
(req, res) => res.json({ ok: true }),
)

app.get(
'/admin',
featureGate({
featureKey: ['admin', 'panel-v2'],
requirement: 'all',
redirectTo: '/upgrade',
}),
(req, res) => res.json({ admin: true }),
)

Disabled by default → 404 JSON. Override with onDisabled, redirectTo, or negate: true.

Also available: featureRoutes([...]), withFeature(key, handler, options?), featuresHandler(), getExpressToggly().

Core options (verifySignatures, cache, streaming) are documented under Configuration.

Feature snapshot endpoint​

featuresHandler() is a factory returning an Express handler. Register it after the middleware:

app.get('/features', featuresHandler())

The response pairs the shared client's boolean feature snapshot with the current request's identity. It does not re-evaluate that snapshot using the request's groups, claims, or Order entity. An identity in the response therefore does not mean the returned flags were evaluated for that user. Use the request's isFeatureOn(key) helper for request-targeted decisions, or its client with explicit context/entity arguments as shown above.