|
| 1 | +--- |
| 2 | +title: 'Use variant feature flags in a Node.js application' |
| 3 | +titleSuffix: Azure App configuration |
| 4 | +description: In this tutorial, you learn how to use variant feature flags in a Node.js application |
| 5 | +#customerintent: As a user of Azure App Configuration, I want to learn how I can use variants and variant feature flags in my Node.js application. |
| 6 | +author: zhiyuanliang-ms |
| 7 | +ms.author: zhiyuanliang |
| 8 | +ms.service: azure-app-configuration |
| 9 | +ms.devlang: javascript |
| 10 | +ms.topic: how-to |
| 11 | +ms.date: 06/06/2025 |
| 12 | +--- |
| 13 | + |
| 14 | +# Tutorial: Use variant feature flags in a Node.js application |
| 15 | + |
| 16 | +In this tutorial, you use a variant feature flag to manage experiences for different user segments in an example application, *Quote of the Day*. You utilize the variant feature flag created in [Use variant feature flags](./howto-variant-feature-flags.md). Before proceeding, ensure you create the variant feature flag named *Greeting* in your App Configuration store. |
| 17 | + |
| 18 | +## Prerequisites |
| 19 | + |
| 20 | +* [LTS versions of Node.js](https://github.com/nodejs/release#release-schedule). |
| 21 | +* Follow the [Use variant feature flags](./howto-variant-feature-flags.md) tutorial and create the variant feature flag named *Greeting*. |
| 22 | + |
| 23 | +## Create a Node.js application |
| 24 | + |
| 25 | +1. Create a folder called `quote-of-the-day` and initialize the project. |
| 26 | + |
| 27 | + ```bash |
| 28 | + mkdir quote-of-the-day |
| 29 | + cd quote-of-the-day |
| 30 | + npm init -y |
| 31 | + ``` |
| 32 | + |
| 33 | +1. Install the following packages. |
| 34 | + |
| 35 | + ```bash |
| 36 | + npm install @azure/app-configuration-provider |
| 37 | + npm install @microsoft/feature-management |
| 38 | + npm install express |
| 39 | + ``` |
| 40 | + |
| 41 | +1. Create a new file named *server.js* and add the following code. |
| 42 | + |
| 43 | + ```js |
| 44 | + const express = require("express"); |
| 45 | + const server = express(); |
| 46 | +
|
| 47 | + const appConfigEndpoint = process.env.AZURE_APPCONFIG_ENDPOINT; |
| 48 | + const { DefaultAzureCredential } = require("@azure/identity"); |
| 49 | + const { load } = require("@azure/app-configuration-provider"); |
| 50 | + const { FeatureManager, ConfigurationMapFeatureFlagProvider } = require("@microsoft/feature-management"); |
| 51 | +
|
| 52 | + let appConfig; |
| 53 | + let featureManager; |
| 54 | + async function initializeConfig() { |
| 55 | + appConfig = await load(appConfigEndpoint, new DefaultAzureCredential(), { |
| 56 | + featureFlagOptions: { |
| 57 | + enabled: true, |
| 58 | + refresh: { |
| 59 | + enabled: true |
| 60 | + } |
| 61 | + } |
| 62 | + }); |
| 63 | +
|
| 64 | + const featureFlagProvider = new ConfigurationMapFeatureFlagProvider(appConfig); |
| 65 | + featureManager = new FeatureManager(featureFlagProvider); |
| 66 | + } |
| 67 | +
|
| 68 | + function startServer() { |
| 69 | + // Use a middleware to refresh the configuration before each request |
| 70 | + server.use((req, res, next) => { |
| 71 | + appConfig.refresh(); |
| 72 | + next(); |
| 73 | + }); |
| 74 | + server.use(express.json()); |
| 75 | + // Serve static index.html from the current folder |
| 76 | + server.use(express.static(".")); |
| 77 | +
|
| 78 | + // This API returns the different greeting messages based on the segment the user belongs to. |
| 79 | + // It evaluates a variant feature flag based on user context. The greeting message is retrieved from the variant configuration. |
| 80 | + server.get("/api/getGreetingMessage", async (req, res) => { |
| 81 | + const { userId, groups } = req.query; |
| 82 | + const variant = await featureManager.getVariant("Greeting", { userId: userId, groups: groups ? groups.split(",") : [] }); |
| 83 | + res.status(200).send({ message: variant?.configuration }); |
| 84 | + }); |
| 85 | +
|
| 86 | + server.post("/api/like", (req, res) => { |
| 87 | + const { UserId } = req.body; |
| 88 | + if (UserId === undefined) { |
| 89 | + return res.status(400).send({ error: "UserId is required" }); |
| 90 | + } |
| 91 | + // Here you would typically emit a 'like' event to compare variants. |
| 92 | + res.status(200).send(); |
| 93 | + }); |
| 94 | +
|
| 95 | + const port = "8080"; |
| 96 | + server.listen(port, () => { |
| 97 | + console.log(`Server is running at http://localhost:${port}`); |
| 98 | + }); |
| 99 | + } |
| 100 | +
|
| 101 | + // Initialize the configuration and start the server |
| 102 | + initializeConfig() |
| 103 | + .then(() => { |
| 104 | + startServer(); |
| 105 | + }) |
| 106 | + .catch((error) => { |
| 107 | + console.error("Failed to load configuration:", error); |
| 108 | + process.exit(1); |
| 109 | + }); |
| 110 | + ``` |
| 111 | +
|
| 112 | +1. Create a new file named *index.html* and add the following code: |
| 113 | +
|
| 114 | + ```html |
| 115 | + <!DOCTYPE html> |
| 116 | + <html lang="en"> |
| 117 | + <head> |
| 118 | + <meta charset="UTF-8"> |
| 119 | + <title>Quote of the Day</title> |
| 120 | + <style> |
| 121 | + .heart-button { |
| 122 | + background-color: transparent; |
| 123 | + border: none; |
| 124 | + cursor: pointer; |
| 125 | + font-size: 24px; |
| 126 | + } |
| 127 | +
|
| 128 | + .heart-button:hover { |
| 129 | + background-color: #F0F0F0; |
| 130 | + } |
| 131 | + </style> |
| 132 | + </head> |
| 133 | + <body> |
| 134 | + <div style="display: flex; flex-direction: column; min-height: 100vh; background-color: #f4f4f4;"> |
| 135 | + <header style="background-color: white; border-bottom: 1px solid #eaeaea; display: flex; justify-content: space-between; align-items: center; font-family: 'Arial', sans-serif; font-size: 16px;"> |
| 136 | + <div style="font-size: 1.25em; color: black;">QuoteOfTheDay</div> |
| 137 | + </header> |
| 138 | +
|
| 139 | + <main style="display: flex; justify-content: center; align-items: center; flex-grow: 1;"> |
| 140 | + <div style="background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); max-width: 700px; position: relative; text-align: left;"> |
| 141 | + <div id="quote-content"> |
| 142 | + <h2 id="greeting">Quote of the Day</h2> |
| 143 | + <blockquote style="font-size: 2em; font-style: italic; color: #4EC2F7; margin: 0 0 20px 0; line-height: 1.4;"> |
| 144 | + <p>"You cannot change what you are, only what you do."</p> |
| 145 | + <footer style="font-size: 0.55em; color: black; font-family: 'Arial', sans-serif; font-style: normal; font-weight: bold;">— Philip Pullman</footer> |
| 146 | + </blockquote> |
| 147 | + <div style="position: absolute; top: 10px; right: 10px; display: flex;"> |
| 148 | + <button class="heart-button" id="like-button"> |
| 149 | + <span id="heart-icon" style="color: #ccc">♥</span> |
| 150 | + </button> |
| 151 | + </div> |
| 152 | + </div> |
| 153 | + <div id="loading" style="display: none;"> |
| 154 | + <p>Loading</p> |
| 155 | + </div> |
| 156 | + </div> |
| 157 | + </main> |
| 158 | + </div> |
| 159 | +
|
| 160 | + <script> |
| 161 | + // extract URL parameters to simulate user login |
| 162 | + document.addEventListener('DOMContentLoaded', function() { |
| 163 | + const urlParams = new URLSearchParams(window.location.search); |
| 164 | + const currentUser = urlParams.get('userId') || ''; |
| 165 | + let liked = false; |
| 166 | + |
| 167 | + const greetingElement = document.getElementById('greeting'); |
| 168 | + const heartIcon = document.getElementById('heart-icon'); |
| 169 | + const likeButton = document.getElementById('like-button'); |
| 170 | + const quoteContent = document.getElementById('quote-content'); |
| 171 | + const loadingElement = document.getElementById('loading'); |
| 172 | +
|
| 173 | + async function init() { |
| 174 | + quoteContent.style.display = 'none'; |
| 175 | + loadingElement.style.display = 'block'; |
| 176 | + |
| 177 | + const response = await fetch(`/api/getGreetingMessage?userId=${currentUser}`, { |
| 178 | + method: "GET" |
| 179 | + }); |
| 180 | + const result = await response.json(); |
| 181 | + greetingElement.textContent = result.message || ""; |
| 182 | + quoteContent.style.display = 'block'; |
| 183 | + loadingElement.style.display = 'none'; |
| 184 | + } |
| 185 | +
|
| 186 | + likeButton.addEventListener('click', async function() { |
| 187 | + if (!liked) { |
| 188 | + const response = await fetch("/api/like", { |
| 189 | + method: "POST", |
| 190 | + headers: { "Content-Type": "application/json" }, |
| 191 | + body: JSON.stringify({ UserId: currentUser }), |
| 192 | + }); |
| 193 | + } |
| 194 | + liked = !liked; |
| 195 | + heartIcon.style.color = liked ? 'red' : '#ccc'; |
| 196 | + }); |
| 197 | +
|
| 198 | + init(); |
| 199 | + }); |
| 200 | + </script> |
| 201 | + </body> |
| 202 | + </html> |
| 203 | + ``` |
| 204 | +
|
| 205 | + For simplicity, the example extracts the `userId` from URL query parameters (e.g., `?userId=UserA`) to simulate different user identities. |
| 206 | +
|
| 207 | +## Run the application |
| 208 | +
|
| 209 | +1. Set the environment variable named **AZURE_APPCONFIG_ENDPOINT** to the endpoint of your App Configuration store found under the *Overview* of your store in the Azure portal. |
| 210 | +
|
| 211 | + If you use the Windows command prompt, run the following command and restart the command prompt to allow the change to take effect: |
| 212 | +
|
| 213 | + ```cmd |
| 214 | + setx AZURE_APPCONFIG_ENDPOINT "<endpoint-of-your-app-configuration-store>" |
| 215 | + ``` |
| 216 | +
|
| 217 | + If you use PowerShell, run the following command: |
| 218 | +
|
| 219 | + ```powershell |
| 220 | + $Env:AZURE_APPCONFIG_ENDPOINT = "<endpoint-of-your-app-configuration-store>" |
| 221 | + ``` |
| 222 | +
|
| 223 | + If you use macOS or Linux, run the following command: |
| 224 | +
|
| 225 | + ```bash |
| 226 | + export AZURE_APPCONFIG_ENDPOINT='<endpoint-of-your-app-configuration-store>' |
| 227 | + ``` |
| 228 | +
|
| 229 | +1. Run the application. |
| 230 | +
|
| 231 | + ```bash |
| 232 | + node server.js |
| 233 | + ``` |
| 234 | +
|
| 235 | +1. Open your browser and navigate to `localhost:8080`. You should see the default view of the app that doesn't have any greeting message. |
| 236 | +
|
| 237 | + :::image type="content" source="media/howto-variant-feature-flags-javascript/default-variant.png" alt-text="Screenshot of the Quote of the day app, showing no greeting message for the user."::: |
| 238 | +
|
| 239 | +1. You can use `userId` query parameter in the url to specify the user ID. Visit `localhost:8080/?userId=UserA` and you see a long greeting message. |
| 240 | +
|
| 241 | + :::image type="content" source="media/howto-variant-feature-flags-javascript/long-variant.png" alt-text="Screenshot of the Quote of the day app, showing long greeting message for the user."::: |
| 242 | +
|
| 243 | +1. Try different user IDs to see how the variant feature flag changes the greeting message for different segments of users. Visit `localhost:8080/?userId=UserB` and you see a shorter greeting message. |
| 244 | +
|
| 245 | + :::image type="content" source="media/howto-variant-feature-flags-javascript/simple-variant.png" alt-text="Screenshot of the Quote of the day app, showing simple greeting message for the user."::: |
| 246 | +
|
| 247 | +## Next steps |
| 248 | +
|
| 249 | +For the full feature rundown of the JavaScript feature management library, refer to the following document. |
| 250 | +
|
| 251 | +> [!div class="nextstepaction"] |
| 252 | +> [JavaScript Feature Management](./feature-management-javascript-reference.md) |
0 commit comments