Feature Flag and Toggle with Unleash
Let’s assume, you are working on a project. Then, you have got a great idea to implement a feature but you want to experiment with it first. Likewise, you already have a feature, but you want to revamp it without breaking the existing one. In addition to this, you have different variants of the same feature. So, to minimize risk and streamline the testing and deployment process, feature flag management comes into play. Here, in this post, I implement a feature flag and toggle with Unleash on both server and UI.
Lab setup
For demonstration, I have implemented a server-side email validation tool that I manage through Unleash. I have the following setup of the project with my Turborepo starter.
- Turoborepo for monorepo
- pnpm package manager
- Monorepo
- Shared React UI library with tailwindcss
- Nextjs for frontend on port 3000
- Expressjs for backend on port 8000
- Unleash server on port 4242
- PostgreSQL server on port 5432
- Unleash client for Express.js
- Unleash proxy for Next.js
Set up Unleash server
First of all, we have to host the unleash server. This should always run as the features depend on this server. For this, I ran the server on port 4242.
require("dotenv").config();
const unleash = require("unleash-server");
unleash
.start({
db: {
ssl: false,
host: process.env.UNLEASH_DB_HOST,
port: +process.env.UNLEASH_DB_PORT,
database: process.env.UNLEASH_DB,
user: process.env.UNLEASH_DB_USER,
password: process.env.UNLEASH_DB_PASS,
},
server: {
port: +process.env.UNLEASH_SERVER_PORT,
},
})
.then((unleash) => {
console.log(
`Unleash started on http://localhost:${unleash.app.get("port")}`
);
});
Once we run this, it creates a user with credentials “admin:unleash4all”. We can log into the server using these credentials to get the following screen.

Using this dashboard, we can manage our features. So, let’s create a feature “email-validator” using the “Create feature toggle” button. I will add a standard strategy for this and enable it in the development only.

Now, to access the server from clients, I need to generate an API token. We can do this from “Configure > API Access”.

Here, I have only created a token for the development environment.
Documentation of Unleash Server
Integrate Unleash in Express.js
Once we set up our server, we need to use unleash client for our express.js server. I will put an initialization config for unleash in a different file “lib/unleash.ts”. This helps to import the config in other files. However, we don’t need this as we can directly import it from the package.
/// <reference path="../types/env.d.ts" />
import { initialize } from "unleash-client";
const unleash = initialize({
url: process.env.UNLEASH_API_URL,
appName: "server",
environment: process.env.APP_ENV || "development",
customHeaders: {
Authorization: process.env.UNLEASH_API_KEY,
},
});
export default unleash;
Also, I added the environment declaration file as follows to prevent typescript errors.
namespace NodeJS {
export interface ProcessEnv {
NODE_ENV: string;
APP_ENV: string;
PORT: string;
WEB_URL: string;
UNLEASH_API_URL: string;
UNLEASH_API_KEY: string;
}
}
Then, in the main “index.ts” file of the server, I imported the initialized config. My final index.ts looks as follows.
import dotenv from "dotenv";
dotenv.config();
import express from "express";
import unleash from "./lib/unleash";
unleash.on("synchronized", () => {
console.log(`Synchronized! on ${new Date().toLocaleString()}`);
});
const app = express();
const PORT = +process.env.PORT || 8000;
app.get("/", (_, res) => res.send("Hello from NepCodeX"));
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});
Up to now, I configured unleash. At this point, I have a server that sends “Hello form NepCodeX” on the root path.
Add route for tools
As I said earlier, my main motive for the website Is to create several utilities. For the same, I am creating a validator for email addresses, hypothetically.
Thus, I created a route for “tools”. Inside that route, I added my email validation logic (a very basic one).
import { Router } from "express";
const router = Router();
router.post("/email-validator", (req, res) => {
const pattern = /@/;
const email = req.body.email;
let response: Object;
if (!email) {
response = { code: 400, message: "Email address is required." };
res.status(400).json(response);
} else {
const isValid = pattern.test(email);
if (isValid) {
response = { code: 200, message: "Email address is valid", email: email };
} else {
response = {
code: 200,
message: "Email address is invalid",
email: email,
};
}
res.json(response);
}
});
export default router;
Here, I created a very basic email validator (just to show you something else). Anyway, this validator only validates if the “@” symbol is present. I imported this route and used it as middleware in the main server file.
// snip ...
import toolsRoutes from "./routes/toolsRoutes";
app.get("/", (_, res) => res.send("Hello from NepCodeX"));
app.use("/tools", toolsRoutes);
// snip ...
This way, I can access the route from “POST /tools/email-validator”. So, let’s test the implementation.
❯ curl -XPOST http://localhost:8000/tools/email-validator -d "email=contact@nepcodex.com"
{"code":200,"message":"Email address is valid","email":"contact@nepcodex.com"}
❯ curl -XPOST http://localhost:8000/tools/email-validator -d "email=nepcodex.com"
{"code":200,"message":"Email address is invalid","email":"nepcodex.com"}
❯ curl -XPOST http://localhost:8000/tools/email-validator
{"code":400,"message":"Email address is required."}
Now, it’s time to use our feature toggle.
import { Router } from "express";
import unleash from "../lib/unleash";
const router = Router();
router.post("/email-validator", (req, res, next) => {
if (unleash.isEnabled("email-validator")) {
const pattern = /@/;
const email = req.body.email;
let response: Object;
// redacted
}
next();
});
export default router;
If we toggle between the feature using the dashboard, we get a different result. By default, Node.js SDK polls every 15 seconds for the update.
❯ curl -XPOST http://localhost:8000/tools/email-validator -d "email=contact@nepcodex.com"
{"code":200,"message":"Email address is valid","email":"contact@nepcodex.com"}
❯ curl -XPOST http://localhost:8000/tools/email-validator -d "email=contact@nepcodex.com"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot POST /tools/email-validator</pre>
</body>
</html>
So, if the toggle is off, it behaves like a 404 route. In the other case, the feature is implemented. Now, let’s add a variant that has advanced pattern checking. In the unleash server, I added a variant — advanced and basic as follows.

However, to use this, I have changed the feature type to Experiment from Release. After this, I modified the route controller.
// snip
router.post("/email-validator", (req, res, next) => {
if (unleash.isEnabled("email-validator")) {
const email = req.body.email;
let response: Object;
let pattern: RegExp;
if (!email) {
response = { code: 400, message: "Email address is required." };
res.status(400).json(response);
} else {
let isValid: boolean | undefined;
const variant = unleash.getVariant("email-validator");
if (variant.name === "basic") {
pattern = /@/;
isValid = pattern.test(email);
} else if (variant.name === "advanced") {
pattern =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
isValid = pattern.test(email);
}
// snip
}
next();
});
export default router;
So, after I tried with a payload that is valid for the basic variant but invalid for the other, the result is as follows.
❯ curl -XPOST http://localhost:8000/tools/email-validator -d "email=contact@nepcodex" -H "host: nepcodex.com"
{"code":200,"message":"Email address is invalid","email":"contact@nepcodex"}
❯ curl -XPOST http://localhost:8000/tools/email-validator -d "email=contact@nepcodex" -H "host: nepcodex.com"
{"code":200,"message":"Email address is valid","email":"contact@nepcodex"}
This is just to show you about variant and nothing else. We don’t need this setup for this particular scenario. Here, we can see that it switched to an advanced pattern quickly (because of its high weight). In this way, we can add different strategies for our features. This concludes the server-side setup.
Serving Unleash proxy API for client side
Since our server-side is completed, we need to do the same for the Next.js app. It’s similar to that of the server.
require("dotenv").config();
const { createApp } = require("@unleash/proxy");
const port = +process.env.UNLEASH_PROXY_PORT || 3000;
console.log(process.env.UNLEASH_PROXY_PORT);
const app = createApp({
unleashUrl: process.env.UNLEASH_API_URL,
unleashApiToken: process.env.UNLEASH_API_KEY,
clientKeys: [process.env.WEB_PROXY_SECRET],
refreshInterval: 1000,
});
app.listen(port, () =>
console.log(`Unleash Proxy listening on http://localhost:${port}/proxy`)
);
Here, the clientKeys are those keys that we have to pass in the requests to access this proxy. So, let’s test this implementation.
❯ curl http://localhost:4000/proxy -H "Authorization: b5758cb6fead016da791d69b85532f7d77f07b6a6ff621e111baffd029aeefc5"
{"toggles":[{"name":"email-validator","enabled":true,"variant":{"name":"advanced","enabled":true}}]}
In my request above, the value in the authorization header is the client key for my Next.js app.
Integrate Unleash on Next.js
To request to our server, we need to set up cors. We can do this easily by using the package “cors”. In my Next.js app, I set up a very simple email validator with Tailwind as follows.
import { Button, Card, TextInput } from "ui";
import { ChangeEvent, SyntheticEvent, useEffect, useState } from "react";
const validateEmail = async (email: string) => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_WEB_API_URL}/tools/email-validator`,
{
method: "POST",
body: new URLSearchParams({
email: email,
}),
}
);
return res.json();
};
export default function Web() {
const [textInput, setTextInput] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [response, setResponse] = useState(null);
const [isSuccess, setIsSuccess] = useState(null);
const [isValid, setIsValid] = useState(null);
const [isError, setIsError] = useState(false);
const handleTextInputChange = (event: ChangeEvent<HTMLInputElement>) => {
if (response) {
setResponse(null);
}
setTextInput(event.target.value);
setIsError(null);
};
const handleSubmit = async (event: SyntheticEvent) => {
event.preventDefault();
setIsSubmitting(true);
try {
const response = await validateEmail(textInput);
setResponse(response);
setIsSuccess(true);
} catch (e) {
setIsSuccess(false);
setIsError(true);
} finally {
setIsSubmitting(false);
}
};
useEffect(() => {
if (!isSubmitting && response) {
setIsError(false);
if (response.message.includes("invalid") || response.code !== 200) {
setIsValid(false);
} else {
setIsValid(true);
}
}
}, [isSubmitting, response, isSuccess]);
return (
<>
<Card>
<TextInput
placeholder="Enter some text"
onChange={handleTextInputChange}
value={textInput}
className="col-span-3"
/>
<Button type="submit" onClick={handleSubmit} className="col-span-1">
Validate
</Button>
{response && (
<div className={`text-${isValid ? "purple" : "red"}-500 col-span-4`}>
{response.message}
</div>
)}
{isError && (
<div className="text-red-500 col-span-4">Something went wrong.</div>
)}
</Card>
</>
);
}
The above component will be as follows.

Of course, the code doesn’t look good but it helps me with the purpose of this post. Now, I set up the unleash proxy react client in the main _app.tsx file.
import "../styles/globals.css";
import type { AppProps } from "next/app";
import dynamic from "next/dynamic";
const FlagProvider = dynamic(() => import("@unleash/proxy-client-react"), {
ssr: false,
});
const config = {
url: process.env.NEXT_PUBLIC_UNLEASH_PROXY_URL,
clientKey: process.env.NEXT_PUBLIC_APP_SECRET,
refreshInterval: 15,
appName: "web",
environment: process.env.NEXT_PUBLIC_ENVIRONMENT || "development",
};
function MyApp({ Component, pageProps }: AppProps) {
return (
<FlagProvider config={config}>
<Component {...pageProps} />
</FlagProvider>
);
}
export default MyApp;
Here, you can notice that I have used dynamic import to import the FlagProvider. This ensures that the component is not server-rendered. With this, I can use a hook “useFlag” to check the toggle state. So, let’s show an error if the flag is not used. I refactored the whole code as follows.
import { Button, Card, TextInput } from "ui";
import { ChangeEvent, SyntheticEvent, useEffect, useState } from "react";
import { useFlag } from "@unleash/proxy-client-react";
import Error from "next/error";
// snip
function EmailValidator() {
// Everything moved to this component
}
export default function Web() {
const [isLoading, setIsLoading] = useState(true);
const isEmailValidatorEnabled = useFlag("email-validator");
useEffect(() => {
if (isEmailValidatorEnabled) {
setIsLoading(false);
}
}, [isEmailValidatorEnabled]);
return (
<>
{isEmailValidatorEnabled && <EmailValidator />}
{!isLoading && !isEmailValidatorEnabled && <Error statusCode={404} />}
</>
);
}
Now, we get our expected result. Once we switch the toggle, it automatically enables or disables this feature without the need for refreshing.
Demo
I hope you enjoyed the blog post. Please leave a comment if anything that I should know.
Check my post about ARP spoofing and insecure protocols (Practical Guide).
Repository link: https://github.com/kriss-u/unleash-nepcodex