BFF: Revolutionizing API Communication for Frontend and Backend

Q. Why choose Backend for Frontend? Doesn’t it add unnecessary complexity?

A. Not really. The benefits far outweigh the added layer of complexity, such as:

  1. Tighter Coupling: BFF connects the UI with backend services without exposing backend details directly to the UI.
  2. UI-Specific Logic: Allows for handling frontend-specific needs like error management, pagination, and more.
  3. Performance Optimization: Caches responses to reduce unnecessary backend calls when data hasn’t changed.
  4. Data Aggregation: Combines data from multiple services into a single response, reducing the need for the UI to make multiple API calls.
  5. Tailored Communication: Different UIs can interact with their own BFFs, without sharing the same backend logic.
  6. Network Security: The browser’s network tab only exposes what the BFF wants, mapping only the required fields.
Old way: Angular --> Backend API (micro-services, 3rd party APIs)
BFF way: Angular -> BFF -> Backend API (micro-services, 3rd party APIs)

Below example is where a user logs in, logs out and get their profile
Profile cannot be fetched if not logged in successfully

Node:
npm init -y
npm install express cors cookie-parser jsonwebtoken dotenv express-session

server.js
require("dotenv").config();
const express = require("express");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");
const session = require("express-session");

const app = express();
const PORT = 5000;
const SECRET_KEY = "your_secret_key"; //Use a better key :)

app.use(cors({ origin: "http://localhost:4200", credentials: true }));
app.use(express.json());
app.use(cookieParser());

app.use(session({
    secret: SECRET_KEY,
    resave: false,
    saveUninitialized: false,
    cookie: { secure: false, httpOnly: true } // secure:true in production with HTTPS
}));

// Mock user
const user = { id: 1, username: "testuser", password: "password123" };

// Login Route
app.post("/login", (req, res) => {
    const { username, password } = req.body;
    if (username === user.username && password === user.password) {
        const token = jwt.sign({ id: user.id }, SECRET_KEY, { expiresIn: "1h" });
        req.session.token = token; // store token in session
        return res.json({ message: "Logged in successfully" });
    }
    res.status(401).json({ message: "Invalid credentials" });
});

// Protected Route using session
app.get("/profile", (req, res) => {
    const token = req.session.token;
    if (!token) return res.status(401).json({ message: "Unauthorized" });

    try {
        const decoded = jwt.verify(token, SECRET_KEY);
        res.json({ message: "Access granted", userId: decoded.id });
    } catch (error) {
        res.status(401).json({ message: "Invalid token" });
    }
});

// Logout Route
app.post("/logout", (req, res) => {
    req.session.destroy();
    res.json({ message: "Logged out successfully" });
});

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

And that's it. Run the backend with node server.js

In Angular, create a service
src\app\services\login.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private apiUrl = 'http://localhost:5000';

  constructor(private http: HttpClient) {}

  login(username: string, password: string): Observable<any> {
    return this.http.post(`${this.apiUrl}/login`,
{ username, password }, { withCredentials: true });
  }

  getProfile(): Observable<any> {
    return this.http.get(`${this.apiUrl}/profile`, { withCredentials: true });
  }

  logout(): Observable<any> {
    return this.http.post(`${this.apiUrl}/logout`, {}, { withCredentials: true });
  }
}

Use the service in your Angular component:

HTML:
<h1>HOTEL RESERVATION SYSTEM</h1>
<div>
    <input [(ngModel)]="username" placeholder="Username">
    <input [(ngModel)]="password" type="password" placeholder="Password">
    <button (click)="login()">Login</button>
    <button (click)="getProfile()">getProfile</button>
    <button (click)="logout()">logout</button>
</div>
<router-outlet />


Component
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { UserServiceLibraryService } from 'kavya-myuser-service-library';
import { AuthService } from './services/login.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, FormsModule],
  providers: [UserServiceLibraryService, AuthService],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {
  title = 'hotel-reservation-system';
  username = '';
  password = '';
  constructor(private authService: AuthService) { }
  ngOnInit() {
  }

  login() {
      this.authService.login(this.username, this.password).subscribe(res => {
        alert("logged in")
      })
  }

  getProfile() {
    this.authService.getProfile().subscribe(res => {
      console.log(res)
    })
  }

  logout() {
    this.authService.logout().subscribe(res => {
      console.log(res)
    })
  }

}


Output:
remember to give {username: "testuser", password: "password123"} for a successful login



The JWT token is fully managed by httpOnly cookie, which is stored in the express-session and expires in 1hr. Front-End doesn't ever see it.

The token seen here in the screenshot is just a session id and not the token. Only the BFF knows about the token encapsulation this logic and handling fully from Front-End

Comments