When thinking about release management, sometimes folks tend to hone in on frontend use cases. Deploying a new version of a UI, adding new components or functionality to a web page, or making edits to copy or visuals.
All of these are valid use cases for LaunchDarkly feature flags, but feature management goes beyond just supporting frontend, visible changes. LaunchDarkly supports a number of SDKs for languages more often associated with APIs, ex: Rust, Go, Javascript, Python, etc.
In this article, we’ll walk through some different ways you can use LaunchDarkly to roll out new APIs and version existing ones.
Using frontend flags
I know. You might be thinking, “wait a minute, earlier you said this article wasn’t about frontend flags!?” If you are thinking that, stick with me, I promise it will all make sense.
While the use case we’re discussing here is about managing backend changes, those changes will most likely impact our frontend. If your organization is already using LaunchDarkly for managing frontend changes, you already have a process in place for flag evaluations and it makes sense to plug into that process vs. creating a brand new one.
That said, you could create a separate project in LaunchDarkly if you are concerned about who has access to these types of flags or you are looking to set up different targeting segments. For the API deployment, there are two methods with frontend flags that we can follow: dynamic address values and dynamic request functions.
Dynamic address values
Let’s look at a typical API call that you might have in your code base. For the purposes of this article, I’m going to be using React code examples, but as I mentioned earlier LaunchDarkly supports many more SDKs, for more information about creating flags in your language of choice, please read our documentation. Here’s a function for submitting a todo item via our API to a database:
const [description, setDescription] = useState('');
const onButtonClick = async (event) => {
try {
const body = { description };
const response = await fetch('http://localhost:5000/api', {
method: 'POST',
mode: 'cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
} catch (error) {
console.error(error.message);
}
};
The key part of this code example is the fetch(‘http://localhost:5000/api’,...
part of the function. Here we’ve hard coded the destination address, but this could easily be modified to retrieve a dynamic string value from a flag instead. By replacing this code with `fetch(${apiAddress})`,...
, we could set a flag to pass in whatever URL we plan to deploy the new API at. Additionally, this opens us up to enabling targeting rules or progressive rollouts of the new API. In this case our code would now look like this:
const onButtonClick = async (event) => {
try {
const body = { description };
const response = await fetch(`${apiAddress}`, {
method: 'POST',
mode: 'cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
} catch (error) {
console.error(error.message);
}
};
This type of change is great if we’re simply planning to migrate to a new API. Maybe it’s because we’re shifting from a Javascript Express API to a Go API and the request logic is going to stay the same, the destination is the only thing that’s changing. But what if we also want to make changes to the request logic? Fortunately, we can do that as well.
Dynamic request functions
In some ways, this process is actually easier than what we described above. If we are going to make large changes to not only which API the frontend is interacting with, but also how that interaction takes place, we could use a traditional boolean flag. Here’s an example, let’s say you didn’t previously have CORS enabled in the request function and you wanted to add it. Maybe you want to test the impact of that change before rolling it out complete, in that case the code could look something like this:
if (enableCORS) {
const onButtonClick = async (event) => {
try {
const body = { description };
const response = await fetch('http://localhost:5000/api', {
method: 'POST',
mode: 'cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
} catch (error) {
console.error(error.message);
}
};
}
else {
const onButtonClick = async (event) => {
try {
const body = { description };
const response = await fetch('http://localhost:5000/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
} catch (error) {
console.error(error.message);
}
};
}
All that’s happening here is that I’m telling my application to assess the value of the flag enableCORS
and if it’s true, serve the function that includes the CORS mode, otherwise serve the function without it. Using boolean flags in this manner makes it easy to iterate on code while limiting the users who can see the changes, letting you test in production. However, we can actually combine both these methods to achieve flag inception! Or flags behind flags.
Flags behind flags
A flag within a flag, just like a dream within a dream. We can dive into the depths of the code and plant feature ideas that will blossom into grand new capabilities! Melodrama aside, it’s a great way to make multiple iterations to our API functions with maximum control. Let’s take a look at an example of what this could look like and then step through it:
if (controlSwitch) {
const onButtonClick = async (event) => {
try {
const body = { description };
const response = await fetch(`${apiAddress}`, {
method: `${method}`,
mode: 'cors',
headers: { `${header}` },
body: JSON.stringify(body),
});
} catch (error) {
console.error(error.message);
}
};
}
else {
const onButtonClick = async (event) => {
try {
const body = { description };
const response = await fetch('http://localhost:5000/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
} catch (error) {
console.error(error.message);
}
};
}
Here we’ve gone a couple layers deep with our flags. First, we have our controlSwitch
flag, just like our previous boolean example we can use this to enable the new code when we feel it’s ready for testing. We can also put targeting rules in place that control which users gain access to this new API functionality. Within the code, we have three flags: apiAddress
, method
, and header
. In this case, both apiAddress
and method
will likely be string flags and header
will be a JSON flag.
The purpose here would be if we were testing out new variations in how our frontend interacts with the API. As we explained in the first section, maybe we’re migrating to a new API and we want to dynamically update the address while keeping the same methods in place, but maybe in addition to migrating APIs we’re also changing the method used and the header presented at the time of the request. Putting flags here would allow us to test different combinations for different contexts.
Maybe mobile users are directed to one API that only allows GET requests. Maybe Chrome browser users have to pass in a specific header in order to access the API. We have many more options by replacing static values with dynamic flag values. Also, we can add prerequisites for these flags to prevent possible misconfigurations, for instance, maybe we require a specific value for apiAddress
gets served in order to change the method
flag. Ultimately we have a great deal of control from the frontend, but what about on the API side?
Making changes in the API
Controlling frontend interactions with an API is a good starting point for feature flags. Likely your organization has some sort of feature management solution in place and so you’re just expanding that existing workflow, but sometimes it makes sense to control variations and updates in the API code itself and fortunately with LaunchDarkly, we can do just that.
As was the case in our frontend examples, we might be trying to update the logic of how our API handles requests coming from various sources. Some examples could include query logic, database locations, or interactions with third party services. In any of these cases, we can use LaunchDarkly flags to control who has the ability to access the API’s functionality and how that functionality operates.
Dynamically handling query logic
A major purpose of APIs is determining how data requests are handled. Whether you’re using REST or GraphQL, the API acts as the plumbing for retrieving and resolving queries for your end users. We can use feature flags for iterating on query logic and test various scenarios before fully committing changes to production environments. Here’s an example of what this could look like, note I’m going to be showing an example of a Node Express API, but please review our SDK documentation for other examples.
app.post('/api', async (req, res) => {
const apiFlag = await client.variation(
"apiFlag",
user,
false
)
if (apiFlag) {
let todos;
try {
todos = await collection.insertOne(req.body);
} catch (e) {
console.error(error.message);
}
} else {
try {
const { description } = req.body;
const newTodos = await pool.query(
'INSERT INTO todos (description) VALUES($1) RETURNING *',
[description],
);
res.json(newTodos.rows[0]);
} catch (error) {
console.error(error.message);
}
}
});
This code block is determining how we want to handle a POST request to our /api
route. Upon the request, we are instructing Node to assess the value of a flag called apiFlag
and take a certain action depending on what value is presented. In this case, it’s a boolean flag. If the flag returns a true value, we want to follow the logic for inserting a new document into a MongoDB Atlas database, if it’s false we will add a new value to a Postgres database using SQL. This code could easily be updated to perform different functionality depending on the flag value. For example, maybe this is all going to the MongoDB instance and our flag is going to alternate between the ability to insert a single document or multiple documents depending on the flag value.
Adding functionality through flags
Beyond handling routes we can expand or narrow the scope of the role that feature flags play in our APIs. As an example, LaunchDarkly flags could be used to gate off new functionality that we intend to use for our API without affecting the basic functionality already in place. Let’s say, we’re thinking about introducing Datadog logging into our application. Even though the metrics being collected will be viewed primarily by the operations and development teams, we want to ensure that the new capabilities don’t trigger any issues. Here’s an example of what this could look like:
const { createLogger, format, transports } = require('winston');
const logMode = client.variation(
logMode,
anonymous,
false
)
if (logMode) {
const logger = createLogger({
level: 'info',
exitOnError: false,
format: format.json(),
transports: [
new transports.File({ filename: `${appRoot}/logs/<FILE_NAME>.log` }),
],
});
module.exports = logger;
// Example logs
logger.log('info', 'Hello simple log!');
logger.info('Hello log with metas',{color: 'blue' });
}
The majority of this code is for setting up the logger
functionality, but we’re wrapping it in a flag called logMode
. This could allow us to roll out the logging feature to our internal development team to ensure that we’re aggregating the metrics properly in our Datadog dashboard before making it more broadly available to say our SRE team. It also lets us see if enabling this functionality will trigger any other issues elsewhere in the code.
Remember, LaunchDarkly feature flags can also pass in dynamic values in the forms of strings, integers, or JSON objects. This means that anywhere we can leverage dynamic values, we can utilize a LaunchDarkly flag. Let’s say we wanted to create different loggers depending on where the user was logging in from. We could use a string
value flag to call specific loggers depending on the user information we retrieve. Here’s an example of how we could use flags to call two different loggers:
const loggerLocation = client.variation(
loggerLocation,
user,
false
)
export const loggerEast = createLogger({
level: 'info',
exitOnError: false,
format: format.json(),
transports: [
new transports.File({ filename: `${appRoot}/logs/<FILE_NAME>.log` }),
],
});
export const loggerWest = createLogger({
level: 'info',
exitOnError: false,
format: format.json(),
transports: [
new transports.File({ filename: `${appRoot}/logs/<FILE_NAME>.log` }),
],
});
if (loggerLocation == "east") {
// Example logs
loggerEast.log('info', 'Hello east logs!');
loggerEast.info('Hello east log with metas',{color: 'blue' });
}
else {
loggerWest.log('info', 'Hello west logs!')
loggerWest.log('Hello west logs with metas', {color: 'red'})
}
In this example, we’re doing two things, first we’re retrieving the flag value for a flag called loggerLocation
, then we’re using the value of that flag to determine which of our two logger functions we call, loggerEast
or loggerWest
. This is just storing dummy logs, but you could see where we could use this in a real world scenario. I might introduce regionalized dashboards to be able to better target issues in my applications. I could use targeting rules to automate which logger gets called based on the context of my users without having to touch the API code.
Conclusion
We’ve only scratched the surface on how LaunchDarkly can be used for managing changes to your APIs. We’ve discussed controlling changes from both inside the API code itself as well as making changes in the Frontend code. The great thing about LaunchDarkly’s SDK support is that feature flags can be integrated into many different types of APIs and applications. For more information be sure to check out our SDK documentation.