unleash feature toggle and flag management in Nextjs, Expressjs and Turborepo

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.

Admin dashboard of unleash server

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.

Toggle enabled in development

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

API Token for development

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.

Variants for the toggle

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.

The email validator’s view

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

https://youtu.be/X7kkeSoPuW4
Demo of the app

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

5 7 votes
Article Rating
Subscribe
Notify of
guest
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Scroll to top

Send help to Morocco.

X