BFF / Frontend workshop

๐Ÿ’ก Project templates to compare stacks around the BFF & SPA pattern (Backend For Frontend & Single Page Application)

Repository Github

Stacks

ASP NET - Blazor

Express - Angular

Nextjs - React

Nuxt - Vue

Actix - Yew (coming soon ...)

BFF (Backend For Frontend):

BFF, short for Backend For Frontend, is a software architectural pattern used in the development of web applications. It involves the creation of dedicated backend services tailored to specific frontend applications or client devices. The primary purpose of a BFF is to optimize the communication between the frontend and backend components by providing tailored endpoints and data structures that precisely match the requirements of the frontend application.

Key Features:

  1. Customized API Endpoints: A BFF exposes a set of API endpoints specifically designed to fulfill the needs of the frontend application. These endpoints are structured to provide only the data and functionality required by the frontend, minimizing unnecessary data transfer and processing.

  2. Optimized Data Formatting: BFFs format data in a way that is most suitable for consumption by the frontend, reducing the need for additional processing or transformation on the client side. This includes tasks such as data aggregation, filtering, and pagination.

  3. Performance Enhancement: By tailoring backend services to the requirements of the frontend, BFFs can improve the performance of web applications. This optimization reduces latency and enhances the overall responsiveness of the user interface.

  4. Security and Authorization: BFFs enforce security measures and access control mechanisms specific to the frontend application. This ensures that only authorized users can access the data and functionality exposed by the backend services.

  5. Aggregation of Backend Services: In cases where the frontend application needs to interact with multiple backend services, a BFF can serve as an aggregation layer, consolidating data and functionality from various sources into a single interface for the client.

  6. Flexibility and Scalability: BFF architecture allows for flexibility in adapting backend services to the evolving needs of the frontend application. It also facilitates the scalability of the system by enabling independent scaling of backend services tailored to different frontend components or versions.

  7. Improved Development Workflow: Separating backend services based on frontend requirements simplifies the development workflow by enabling frontend and backend teams to work more independently. This leads to faster iterations, easier debugging, and better overall collaboration between teams.

In summary, BFF (Backend For Frontend) is a software architectural pattern that enhances the performance, security, and development workflow of web applications by tailoring backend services to the specific needs of frontend applications or client devices.

Specification

To add a "BFF/Frontend" stack reference to this repository, here is the following specifications :

Material UI

  • Add material UI design system
    • color primary by techno
    • layout with app bar & collapsable sidemenu to rail

Custom endpoint

  • Expose custom server endpoint to "/version"
    • return { version: "1.0.0" }

Server side rendering

  • Expose frontend web application with SSR
    • pre-rendering page with data
    • fallback into interactive UI
  • Handle OIDC auth over keycloak SSO
  • Cookie & antiforgery token
  • Login / Logout clean process

Proxying API

GraphQL gateway

Auto generated SDK

  • SDK to consume BFF GraphQL schema
    • Auto generate it from BFF url
    • Use it in frontend application

Light / Dark theme

  • Theme switcher implementation

I18N

  • I18N switcher : FR & EN

Feature management

  • BFF expose feature management configuration
    • Enable / disable flag from config and show "User" page link in sidemenu accordingly (A/B testing)

Roadmap

ASP.NET - Blazor

  • โœ… Material UI
  • โœ… Custom endpoint
  • โœ… Server side rendering
  • โœ… OIDC / Cookie authentication
  • โœ… Proxying API
  • โœ… GraphQL gateway
  • โœ… Auto generated SDK
  • โœ… Light / Dark theme
  • โœ… I18N
  • โœ… Feature management

Express - Angular

  • โœ… Material UI
  • โœ… Custom endpoint
  • โœ… Server side rendering
  • โœ… OIDC / Cookie authentication (to ๐Ÿงน)
  • โœ… Proxying API
  • ๐Ÿ› ๏ธ GraphQL gateway
  • ๐Ÿšซ Auto generated SDK
  • ๐Ÿšซ Light / Dark theme
  • ๐Ÿšซ I18N
  • โœ… Feature management

Nuxt - Vue

  • โœ… Material UI
  • โœ… Custom endpoint
  • โœ… Server side rendering
  • ๐Ÿ› ๏ธ OIDC / Cookie authentication
  • โœ… Proxying API
  • ๐Ÿšซ GraphQL gateway
  • ๐Ÿšซ Auto generated SDK
  • ๐Ÿšซ Light / Dark theme
  • ๐Ÿšซ I18N
  • ๐Ÿšซ Feature management

Nextjs - React

  • ๐Ÿ› ๏ธ Material UI
  • โœ… Custom endpoint
  • โœ… Server side rendering
  • ๐Ÿšซ OIDC / Cookie authentication
  • ๐Ÿ› ๏ธ Proxying API
  • ๐Ÿšซ GraphQL gateway
  • ๐Ÿšซ Auto generated SDK
  • ๐Ÿšซ Light / Dark theme
  • ๐Ÿšซ I18N
  • ๐Ÿšซ Feature management

Yew - Rust (coming)

  • ๐Ÿšซ Material UI
  • ๐Ÿšซ Custom endpoint
  • ๐Ÿšซ Server side rendering
  • ๐Ÿšซ OIDC / Cookie authentication
  • ๐Ÿšซ Proxying API
  • ๐Ÿšซ GraphQL gateway
  • ๐Ÿšซ Auto generated SDK
  • ๐Ÿšซ Light / Dark theme
  • ๐Ÿšซ I18N
  • ๐Ÿšซ Feature management

Contributing

Requirements

  • node 18+ SDK
  • dotnet 8 SDK
  • docker engine

Building documentation

Open documentation using mdbook

cd ./doc
mdbook serve -p 5555

IAC

cd ./IAC
docker-compose up

Go to keycloak realm admin & regenerate clientSecret for "boilerplate" client

Then set all your config file with this secret

Blazor

cd ./src/blazor/Microscope.Boilerplate.Clients.BFF
dotnet run

Angular

cd ./src/angular
npm run dev

Nuxt

cd ./src/nuxt
npm run dev

Nextjs

cd ./src/next
npm run dev

Blazor

Blazor is a .NET frontend web framework that supports both server-side rendering and client interactivity in a single programming model

../public/blazor.png

Features

Material UI

  • Add material UI design system
    • color primary by techno
    • layout with app bar & collapsable sidemenu to rail

๐Ÿ’ก Easy intergration using MudBlazor. Excellent DX !

<MudLayout>
    <MudAppBar Elevation="1" Color="Color.Primary" Dense="true">
        <MudIconButton Icon="@Icons.Material.Outlined.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="((e) => ToggleDrawer())"/>
        
        <MudText>Microscope.Boilerplate.NET</MudText>
        @if (!HostingEnvironmentService.IsWebAssembly)
        {
            <MudProgressCircular Class="ml-2" Size="Size.Small" Color="Color.Inherit" Indeterminate="true"></MudProgressCircular>
        }
        <MudSpacer/>
        <MudIconButton Icon="@Icons.Material.Filled.Brightness4" Color="Color.Inherit" OnClick="ToggleTheme"/>
        <MudIconButton Href="https://github.com/bhtz/microscope-boilerplate" Target="_blank" Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit"/>
        <LoginDisplay />
    </MudAppBar>
    
    <MudDrawer @bind-open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Variant="DrawerVariant.Mini" MiniWidth="60px">
        <NavMenu/>
    </MudDrawer>
    
    <MudMainContent Class="mt-16 pa-4">
        @Body
    </MudMainContent>
</MudLayout>
    private bool DrawerOpen { get; set; } = true;

    public void ToggleDrawer()
    {
        DrawerOpen = !DrawerOpen;
    }

Version endpoint

๐Ÿ’ก Expose custom server endpoint to "/version" using a simple asp net minimal API endpoint

app.MapGet("/version", () => new { Version = "1.0.0" });

Server side rendering

  • Expose frontend web application with SSR
    • pre-rendering page with data
    • fallback into interactive UI

๐Ÿ’ก Let blazor handle the magic :

Program.cs

    app.MapRazorComponents<Host>()
        .AddInteractiveServerRenderMode()
        .AddInteractiveWebAssemblyRenderMode()
        .AddAdditionalAssemblies(typeof(_Imports).Assembly);

Host.cs

    <body>
        <Routes @rendermode="InteractiveAuto" />
        <script src="_framework/blazor.web.js"></script>
    </body>
  • Handle OIDC auth over keycloak SSO
  • Cookie & antiforgery token
  • Login / Logout clean process

๐Ÿ’ก Simple OIDC asp net authentication

        services.AddAuthorization();
        services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                options.Authority = oidcAuthenticationOptions.Authority;
                options.ClientId = oidcAuthenticationOptions.ClientId;
                options.ClientSecret = oidcAuthenticationOptions.ClientSecret;

                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.ResponseType = OpenIdConnectResponseType.Code;

                options.RequireHttpsMetadata = false;
                options.SaveTokens = true;
                options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = oidcAuthenticationOptions.NameClaimType,
                    RoleClaimType = oidcAuthenticationOptions.RoleClaimType
                };

                foreach (var item in oidcAuthenticationOptions.Scopes)
                {
                    options.Scope.Add(item);
                }
            });

Proxying API

๐Ÿ’ก Using YARP

services
    .AddReverseProxy()
    .LoadFromConfig(configuration.GetSection("ReverseProxy"));
  "ReverseProxy": {
    "Routes": {
      "post-service" : {
        "ClusterId": "post-service",
        "Match": {
          "Path": "/api/todos/{**catch-all}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/api"
          }
        ]
      }
    },
    "Clusters": {
      "post-service": {
        "Destinations": {
          "post-service": {
            "Address": "https://jsonplaceholder.typicode.com"
          }
        }
      }
    }
  }

GraphQL gateway

๐Ÿ’ก Using Hotchocolate GraphQL gateway

GraphQL configuration

var builder = services.AddGraphQLServer();

foreach (var scalar in gatewayOptions.Scalars)
{
    builder.AddType(new AnyType(scalar));
}

foreach (var schema in gatewayOptions.Schemas)
{
    services
        .AddHttpClient(schema.Name, c => c.BaseAddress = new Uri(schema.Url))
        .AddHttpMessageHandler<BffAuthenticationHeaderHandler>();

    builder.AddRemoteSchema(schema.Name);

    var subgraph = services.AddGraphQL(schema.Name);
    foreach (var scalar in gatewayOptions.Scalars)
    {
        subgraph.AddType(new AnyType(scalar));
    }
}

builder.AddTypeExtensionsFromFile("./stitching.graphql");

appsettings.development.json

  "GraphQLGateway": {
    "Scalars" : ["date", "timestamptz", "uuid"],
    "Schemas": [
      {
        "Name": "Countries",
        "Url": "https://countries.trevorblades.com/"
      }
    ]
  },

Auto generated SDK

  • SDK to consume BFF GraphQL schema
    • Auto generate it from BFF url
    • Use it in frontend application

๐Ÿ’ก Using Hotchocolate Strawberry Shake

cd ./src/blazor/Microscope.Boilerplate.Clients.SDK.GraphQL
dotnet new tool-manifest
dotnet tool install StrawberryShake.Tools
dotnet add package StrawberryShake.Blazor
dotnet graphql init http://localhost:5215/graphql -n BffClient
query GetContinents {
    continents {
        name
    }
}
dotnet build

Add SDK reference to Blazor project

<UseGetContinents Context="result">
    <ChildContent>
        <MudPaper>
            <MudList>
                @foreach (var item in result.Continents)
                {
                    <MudListItem Icon="@Icons.Material.Filled.List">@item.Name</MudListItem>
                }
            </MudList>
        </MudPaper>
    </ChildContent>
    <ErrorContent>
        @result.First().Message
    </ErrorContent>
    <LoadingContent>
        <MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="50px" />
        <MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="50px" Class="mt-4" />
    </LoadingContent>
</UseGetContinents>

Light / Dark theme

  • Theme switcher implementation

๐Ÿ’ก Using MudBlazor "MudThemingProvider"

Defining theme

public static MudTheme DarkTheme = new MudTheme()
{
    Palette = new Palette()
    {
        Black = "#27272f",
        Background = "#32333d",
        BackgroundGrey = "#27272f",
        Primary = Colors.Cyan.Darken3,
        Surface = "#373740",
        DrawerBackground = "#27272f",
        DrawerText = "rgba(255,255,255, 0.50)",
        AppbarBackground = "#27272f",
        AppbarText = "rgba(255,255,255, 0.70)",
        TextPrimary = "rgba(255,255,255, 0.70)",
        TextSecondary = "rgba(255,255,255, 0.50)",
        ActionDefault = "#ffffff",
        ActionDisabled = "rgba(255,255,255, 0.26)",
        ActionDisabledBackground = "rgba(255,255,255, 0.12)",
        DrawerIcon = "rgba(255,255,255, 0.50)"
    }
};

Apply theme

<MudThemingProvider Theme="_currentTheme" />

I18N

  • I18N switcher : FR & EN

๐Ÿ’ก Using resources files & Localization

server side & client side

services.AddLocalization(options => options.ResourcesPath = "Resources" );

set culture endpoint

app.MapGet("/culture", (HttpContext context, string? culture, string? redirectUri) =>
{
    if (culture != null)
    {
        var requestCulture = new RequestCulture(culture, culture);
        var cookieName = CookieRequestCultureProvider.DefaultCookieName;
        var cookieValue = CookieRequestCultureProvider.MakeCookieValue(requestCulture);

        context.Response.Cookies.Append(cookieName, cookieValue);
    }

    return Results.Redirect(redirectUri ?? "/");
});

client side set culture based on cookie (for SSR WASM fallback)

var host = builder.Build();

var cookieService = host.Services.GetRequiredService<CookieService>();
var cultureCookie = await cookieService.GetCultureFromCookie();

if (!string.IsNullOrEmpty(cultureCookie))
{
    var culture = CookieRequestCultureProvider.ParseCookieValue(cultureCookie)?.Cultures.FirstOrDefault().ToString();
    if (culture is not null)
    {
        var cultureInfo = new CultureInfo(culture);
        CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
        CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
    }
}

await host.RunAsync();

razor page

@inject IStringLocalizer<Continents> Loc
<MudText Class="mb-8">@Loc["SubTitle"]</MudText>

Feature management

  • BFF expose feature management configuration
    • Enable / disable flag from config
    • A/B testing use case

๐Ÿ’ก Using Microsoft.FeatureManagement.AspNetCore

client page

public class ClientFeatureManagementService(HttpClient client) : IFeatureManagementService
{
    public async Task<Dictionary<string, bool>?> GetFeatureManagement()
    {
        return await client.GetFromJsonAsync<Dictionary<string, bool>>("/api/features");
    }
}

server page

public class ServerFeatureManagementService(IFeatureManager featureManager) : IFeatureManagementService
{
    public async Task<Dictionary<string, bool>?> GetFeatureManagement()
    {
        return await featureManager.GetFeatureNamesAsync()
            .ToDictionaryAsync(
                feature => feature, 
                feature => featureManager.IsEnabledAsync(feature).GetAwaiter().GetResult());
    }
}

endpoint feature management

public static void MapFeatureManagementEndpoints(this WebApplication app)
{
    app.MapGet("/api/features", async (IFeatureManagementService featureManagementService) => 
        await featureManagementService.GetFeatureManagement());
}

pages or layout

<FeatureFlag FlagName="ShowUserPage">
    <MudNavLink Href="user" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.VerifiedUser">@Loc["User"]</MudNavLink>
</FeatureFlag>

Angular

The web development framework for building the future

../public/angular.png

Features

Material UI

  • Add material UI design system
    • color primary by techno
    • layout with app bar & collapsable sidemenu to rail

๐Ÿ’ก Easy intergration using "@angular/material" package

<mat-toolbar color="primary">
  <mat-icon style="cursor: pointer;" (click)="toggle()" aria-hidden="false" aria-label="Example home icon" fontIcon="menu"></mat-icon>
  <span style="margin-left: 10px; font-weight: normal; font-size: medium;">Microscope.Boilerplate.Angular</span>
  <span class="spacer"></span>
  
  <button *ngIf="!isAuthenticated" mat-icon-button (click)="login()">
    <mat-icon>account_circle</mat-icon>
  </button>

  <button *ngIf="isAuthenticated" mat-button [matMenuTriggerFor]="menu">AD</button>
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="goToAccount()">Account</button>
    <button mat-menu-item (click)="logout()">Logout</button>
  </mat-menu>

</mat-toolbar>

<mat-sidenav-container class="sidenav-container">
  <mat-sidenav class="sidenav" mode="side" [(opened)]="opened" >
    <mat-nav-list>
      <mat-list-item routerLink="/" routerLinkActive="active"><mat-icon class="v-align" fontIcon="home"></mat-icon><span>Home</span></mat-list-item>
      <mat-list-item routerLink="/counter" routerLinkActive="active"><mat-icon class="v-align" fontIcon="add"></mat-icon><span>Counter</span></mat-list-item>
      <mat-list-item routerLink="/posts" routerLinkActive="active"><mat-icon class="v-align" fontIcon="list"></mat-icon><span>Posts</span></mat-list-item>
      <mat-list-item routerLink="/user" routerLinkActive="active"><mat-icon class="v-align" fontIcon="verified_user"></mat-icon><span>User</span></mat-list-item>
    </mat-nav-list>
  </mat-sidenav>
  <mat-sidenav-content class="sidenav-content">
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>
export class AppComponent implements OnInit {
    public opened: boolean = true;
    // ..
    toggle() {
        this.opened = !this.opened;
    }
    // ..
}

Custom endpoint

๐Ÿ’ก Expose custom server endpoint to "/version" using a express API endpoint

server.get('/version', (req, res) => {
    res.json({ version: '1.0.0' })
})

Server side rendering

  • Expose frontend web application with SSR
    • pre-rendering page with data
    • fallback into interactive UI

๐Ÿ’ก Let angular universal handle the magic :

server.ts

  import { CommonEngine } from '@angular/ssr';

  // All regular routes use the Angular engine
  server.get('*', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });
  • Handle OIDC auth over keycloak SSO
  • Cookie & antiforgery token
  • Login / Logout clean process

๐Ÿ’ก Using angular-oauth2-oidc package for angular authentication

๐Ÿšจ Authentication is handle here client side & secret token is in the browser ... to improve

oidc.service.ts

import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';

@Injectable({
    providedIn: 'root',
})
export class AuthService {
    private oAuthService = inject(OAuthService);
    private router = inject(Router);

    constructor() {
        this.initConfiguration();
    }

    initConfiguration() {
        const authConfig: AuthConfig = {
            issuer: 'http://localhost:8083/realms/microscope/',
            clientId: 'boilerplate',
            dummyClientSecret: 'JxaXjmKOd08cMpaKrThAObUzeOmyRiLN',
            scope: 'roles',
            responseType: 'code',
            redirectUri: 'http://localhost:4200/',
            strictDiscoveryDocumentValidation: false,
            skipIssuerCheck: true
        };

        this.oAuthService.configure(authConfig);
        this.oAuthService.setupAutomaticSilentRefresh();
        this.oAuthService.loadDiscoveryDocumentAndTryLogin();
    }

    login() {
        this.oAuthService.initLoginFlow();
    }
    // ..
}

Proxying API

๐Ÿ’ก Using express-http-proxy package

server.ts

import proxy from 'express-http-proxy';
// ...
server.use('/api', proxy('https://jsonplaceholder.typicode.com'));

Feature management

  • BFF expose feature management configuration
    • Enable / disable flag from config
    • A/B testing use case

๐Ÿ’ก Using a config file & API endpoint

./config/default.json

{
    "FeatureManagement": {
        "ShowUserPage": true
    }
}

./server.ts

import config from 'config';
// ...
server.get('/features', (req, res) => {
  let configs = config.get('FeatureManagement') as FeatureFlagResponse;
  res.json(configs);
})

./src/feature-flags.service.ts

@Injectable({ providedIn: 'root' })
export class FeatureFlagService {

    http = inject(HttpClient);
    features = signal<Record<string, boolean>>({});

    loadFeatureFlags(): Observable<FeatureFlagResponse> {
        return this.http.get<FeatureFlagResponse>('/features').pipe(tap((features) => this.features.set(features)));
    }

    getFeature(feature: FeatureFlagKeys): boolean {
        return this.features()[feature] ?? false;
    }
}

type _FeatureFlagKeys = keyof FeatureFlagResponse;
export type FeatureFlagKeys = { [K in _FeatureFlagKeys]: K; }[_FeatureFlagKeys]

export type FeatureFlagResponse = {
    ShowUserPage: boolean;
}

./src/app.component.ts

  private featureFlagService = inject(FeatureFlagService);

  ngOnInit(): void {
    // ...
    this.featureFlagService.loadFeatureFlags().subscribe(()=> this.isUserPageEnabled = this.featureFlagService.getFeature('ShowUserPage'));
  }

./src/app.component.html

  <mat-list-item *ngIf="isUserPageEnabled" routerLink="/user" routerLinkActive="active"><mat-icon class="v-align" fontIcon="verified_user"></mat-icon><span>User</span></mat-list-item>

Next

The React Framework for the Web

../public/nextjs.png

Features

Nuxt

The Intuitive Vue Framework

../public/nuxt.png

Features

Yew

A framework for creating reliable and efficient web applications.

Features