Implementando Feature Flags no Angular

Feature Flags, também conhecidas como Feature Toggles, são uma técnica de desenvolvimento de software que permite ativar ou desativar funcionalidades específicas de um aplicativo sem precisar alterar o código ou reimplantar o aplicativo. Esta técnica é amplamente usada para controlar a exposição de novos recursos, realizar testes A/B, lançar gradualmente funcionalidades e gerenciar diferentes configurações de um software.
Neste artigo, vamos aprender como podemos aproveitar vários recursos do Angular para evitar a entrada ou visualização de áreas proibidas com Feature Flags.
Consumindo os dados do backend
Para enviar as configurações de Feature Flags do backend para o frontend, o backend geralmente retorna uma resposta em formato JSON que contém o estado de cada feature flag. O frontend pode então usar essas informações para habilitar ou desabilitar funcionalidades específicas na interface do usuário.
Aqui está um exemplo de como uma resposta JSON de Feature Flags pode ser estruturada:
{
"featureFlags": {
"newDashboard": true,
"betaFeature": false,
"darkMode": true,
"isAdmin": true,
"isDeveloper": false
}
}
Armazenando os dados ao iniciar a aplicação
A primeira coisa que devemos fazer, antes de qualquer possível interação do usuário, é obter esses dados vindos do backend e armazená-los na memória. Isso previne que alguma operação indevida seja realizada antes que a permissões tenham sido configuradas.
Felizmente, o Angular disponibiliza o Injection Token APP_INITIALIZER
. Este provider retorna uma Promise ou Observable que aguardará ser resolvida para que a aplicação inicie o carregamento dos componentes.
Para entender mais sobre este Token, leia meu artigo:
Vamos criar uma factory function com o APP_INITIALIZER
para obter os dados:
const loadFeatureFlags: FactoryProvider = {
provide: APP_INITIALIZER,
useFactory: () => {
const featureFlagService = inject(FeatureFlagService);
return () => featureFlagService.getFlags();
},
multi: true
};
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
loadFeatureFlags
]
};
Perceba que temos uma Service chamada FeatureFlagService
. Ela precisa ter duas responsabilidades:
- Fazer a chamada HTTP para recuperar os dados do backend
- Armazenar os dados na memória para que seja possível recuperá-los a qualquer momento, mais conhecido como gerenciador de estado.
@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
private http = inject(HttpClient);
private featureFlagStore = inject(FeatureFlagStore);
getFlags() {
return this.http.get('https://meu-backend/feature-flags').pipe(
tap(featureFlags => this.featureFlagStore.setStore(featureFlags))
);
}
}
Criando uma Store
Para ter uma melhor separação de responsabilidades, após a FeatureFlagService
recuperar os dados do backend, podemos salvá-los em uma Service apartada, nesse caso com o nome FeatureFlagStore
. Temos diversas formas de armazenar esse estado no Angular. Podemos utilizar Observables com BehaviorSubjects ou utilizar Signals, que são uma forma mais nova e simplificada de trabalhar com estados no framework.
No nosso exemplo, vamos implementar a Store com BehaviorSubjects:
import { coerceArray } from '@angular/cdk/coercion';
@Injectable({ providedIn: 'root' })
export class FeatureFlagStore {
private storeSubject$ = new BehaviorSubject<FeatureFlag | null>(null);
flags$ = this.storeSubject$.asObservable();
setStore(featureFlags: FeatureFlag) {
this.storeSubject$.next(featureFlags);
}
hasFlags(flags: string | string[]): boolean {
const flags = this.storeSubject$.getValue().featureFlags;
return coerceArray(flags).every(current => userFlags[current]);
}
}
Caso você utilize bibliotecas de gerenciamento de estado como NgRX, NGXS ou Akita, esta Store vai ser implementada com elas. Mas não há obrigatoriedade em utilizar uma biblioteca para isso. O Angular consegue manipular estado nativamente com muita competência.
Atenção: Utilize uma biblioteca de gerenciamento de estado somente quando você sentir que sua aplicação precisa dela. Caso contrário, você estará adicionando complexidade desnecessária ao seu projeto.
Criando diretiva estrutural
As diretivas estruturais são responsáveis pelo layout HTML. Elas moldam a estrutura do DOM, normalmente adicionando, removendo ou manipulando elementos, e é exatamente disso que precisamos.
Entenda mais sobre diretivas estruturais neste meu artigo:
A diretiva estrutural *featureFlag
será responsável por exibir um template fornecido, de forma DRY, dependendo se o usuário está autorizado a esse template. Vamos criá-lo:
@Directive({
selector: '[featureFlag]'
})
export class FeatureFlagDirective {
@Input() featureFlag: string | string[];
@Input() featureFlagOr: string = '';
private vcr = inject(ViewContainerRef);
private tpl = inject(TemplateRef<any>);
private featureFlagStore = inject(FeatureFlagStore);
ngOnInit() {
if (this.featureFlagStore.hasFlags(this.featureFlag)) ||
this.featureFlagStore.hasFlags(this.featureFlagOr){
this.vcr.createEmbeddedView(this.tpl);
}
}
}
Na diretiva, permitimos renderizar um determinado template somente se a Store possuir as Feature Flags recebidas via Input. Para deixar o mais flexível possível, nosso método hasFlags()
da FeatureFlagStore
aceita tanto uma string como um array de strings. Com isso temos o controle de obrigar somente uma feature flag ou várias.
Utilizamos a diretiva de forma bastante elegante:
<main>
<!-- Somente se possuir 'isAdmin' -->
<div *featureFlag="'isAdmin'">
<button>Acessar área administrativa</button>
</div>
<!-- Somente se possuir 'newDashboard' E 'darkMode' -->
<app-dark-dashboard *featureFlag="['newDashboard', 'darkMode']">
</app-dark-dasboard>
<!-- Somente se possuir 'isAdmin' OU 'isDeveloper' -->
<app-products *featureFlag="'isAdmin' or 'isDeveloper'">
</app-products>
</main>
Criando guarda de rota
É muito comum aplicações restringirem o acesso à determinadas rotas com base em uma lógica. As guardas de rotas são perfeitas para este papel. Vamos criar uma guarda de rota que permite o acesso a uma rota com base em nossas Feature Flags.
A partir da versão 14.2 do Angular, temos a possibilidade de criar guardas de rotas funcionais, o que facilita muito o processo. Criando uma função fica muito mais fácil de atribuir as flags de autorização via parâmetro.
export function featureFlagGuard(
featureFlags: string | string[]
): CanActivateFn {
return () => {
const featureFlagStore = inject(FeatureFlagsStore);
const router: Router = inject(Router);
const hasFlags = featureFlagStore.hasFlags(featureFlags);
return hasFlags || router.createUrlTree(['nao-autorizado']);
};
}
No exemplo acima, criamos uma guarda de rota funcional que recebe as Feature Flags por parâmetro. Temos aqui também a flexibilidade de atribuir uma ou várias feature flags.
Injetamos a nossa Store, e o método hasFlags()
faz novamente o papel de verificar se o usuário possui as flags necessárias. Caso contrário, redirecionamos para uma rota não não autorizada.
Agora podemos utilizar a guarda em todas as rotas que precisam dessa validação.
const routes: Route[] = [
{
path: 'admin-area',
component: AdminAreaComponent,
canActivate: [featureFlagGuard('isAdmin')]
},
{
path: 'dark-dashboard',
component: DarkDashboardComponent,
canActivate: [featureFlagGuard(['newDashboard', 'darkMode'])]
}
]
Utilizando via Injeção de dependência
Em alguns casos, será necessário realizar a validação de feature flags diretamente no código de um componente ou de uma service. Para estes cenários, a diretiva e a guarda de rota não serão tão úteis.
A boa notícia é que já temos a Store criada. Ela basicamente é uma Service injetável, que pode ser utilizada em qualquer lugar da nossa aplicação.
Vamos vê-la em ação em um componente:
@Component({
template: `
<a routerLink="route">Acessar página</a>
`
})
export class AppComponent {
featureFlagStore = inject(FeatureFlagStore);
route = this.featureFlagStore.hasFlags('betaFeature') ? 'new-page' : 'old-page';
}
No código acima, nos beneficiamos da Store para verificar qual rota deverá ser acessada com base na flag passada. Injetamos ela diretamente no componente via injeção de dependência.
Conclusão
Neste artigo entendemos o que são Feature Flags e como podemos implementá-las no Angular. Aprendemos a integrar as Feature Flags em diversos momentos para restringir o acesso do usuário, utilizando via diretivas estruturais, guardas de rota e via injeção de dependência.