Skip to content

BFF Pattern Implementation Plan: Bast Admin UI

Date: December 2025
Status: Implementation Guide
Architecture: Hybrid BFF Pattern (Option C)
Related: Backend-API-Architecture-Options.md


Table of Contents

  1. Overview
  2. Prerequisites
  3. Phase 1: Project Setup & Infrastructure
  4. Phase 2: BFF Core Implementation
  5. Phase 3: Authentication & Session Management
  6. Phase 4: First Service Integration
  7. Phase 5: Request Aggregation & Response Shaping
  8. Phase 6: Caching Layer
  9. Phase 7: Real-time Features (WebSockets)
  10. Phase 8: Frontend Integration
  11. Phase 9: Production Readiness
  12. Testing Strategy
  13. Rollback Plan

Overview

This document provides a step-by-step implementation plan for adding a TypeScript BFF (Backend-for-Frontend) layer between the React frontend and existing FastAPI microservices.

Architecture Target

┌─────────────────┐         ┌──────────────────┐         ┌──────────────────┐
│  React Frontend │  tRPC   │  Node.js BFF     │  REST    │  FastAPI Services│
│  (TypeScript)   │◄────────►│  (TypeScript)    │◄────────►│  (Python)        │
│                 │          │                  │          │                  │
│ • TanStack Q    │          │ • NestJS/Express│          │ • Existing       │
│ • tRPC Client  │          │ • tRPC Server    │          │ • No Changes     │
│                 │          │ • Auth/Session   │          │                  │
│                 │          │ • Aggregation    │          │                  │
│                 │          │ • Caching        │          │                  │
│                 │          │ • WebSockets     │          │                  │
└─────────────────┘          └──────────────────┘          └──────────────────┘

Key Benefits

  • ✅ End-to-end type safety via tRPC
  • ✅ Preserves existing FastAPI investment
  • ✅ Frontend team controls API shape
  • ✅ Centralized authentication
  • ✅ Request aggregation for complex views
  • ✅ Real-time capabilities

Prerequisites

Required Knowledge

  • TypeScript/Node.js
  • React & TanStack Query
  • Basic understanding of tRPC
  • REST API concepts
  • Docker (for local development)

Existing Infrastructure

  • ✅ FastAPI microservices running
  • ✅ React frontend application
  • ✅ MongoDB Atlas account (for BFF session storage) - Setup Guide
  • ✅ Redis (for caching - can use Docker for local)

MongoDB Atlas Setup

For Production (MongoDB Atlas):

  1. Create MongoDB Atlas Account
  2. Go to MongoDB Atlas
  3. Sign up for a free account (M0 cluster is free)

  4. Create a Cluster

  5. Choose a cloud provider (AWS, Azure, GCP)
  6. Select a region close to your deployment
  7. Create an M0 (Free) cluster for development

  8. Configure Database Access

  9. Go to "Database Access"
  10. Create a database user with username/password
  11. Set appropriate permissions (read/write)

  12. Configure Network Access

  13. Go to "Network Access"
  14. Add IP address: 0.0.0.0/0 for development (or specific IPs for production)
  15. Or use "Allow Access from Anywhere" for testing

  16. Get Connection String

  17. Go to "Database" → "Connect"
  18. Choose "Connect your application"
  19. Copy the connection string
  20. Format: mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority
  21. Replace <username>, <password>, and <dbname> with your values

  22. Set Environment Variable

    MONGODB_URI=mongodb+srv://<user>:<password>@<cluster>.mongodb.net/bast_bff?retryWrites=true&w=majority
    

For Local Development:

You can use MongoDB locally with Docker:

docker run -d -p 27017:27017 --name mongodb mongo:7

Then use:

MONGODB_URI=mongodb://localhost:27017/bast_bff

Tools & Technologies

Tool Purpose Version
Node.js BFF runtime 18+ LTS
NestJS or Express BFF framework Latest
tRPC Type-safe RPC v11+
@trpc/server tRPC server v11+
@trpc/client tRPC client v11+
@tanstack/react-query Frontend state v5+
Redis Caching layer 7+
MongoDB Atlas Session storage Cloud (or MongoDB 6+)
Mongoose MongoDB ODM Latest
Jest Testing Latest
Supertest API testing Latest

Phase 1: Project Setup & Infrastructure

Goal: Set up the BFF project structure and development environment.

Step 1.1: Create BFF Project Structure

Repository Structure Decision: See Repository-Structure-Decision.md for details. This plan assumes a monorepo structure.

# If restructuring existing repo to monorepo:
# 1. Move current frontend code to apps/admin-ui/
mkdir -p apps/admin-ui
# Move existing src/, package.json, vite.config.ts to apps/admin-ui/

# 2. Create BFF in apps/bff/
mkdir -p apps/bff
cd apps/bff

# Initialize Node.js project
npm init -y

# Install dependencies
npm install @trpc/server@latest @trpc/client@latest
npm install express cors dotenv
npm install zod  # For validation
npm install axios  # For calling FastAPI services
npm install mongoose  # MongoDB ODM for session storage
npm install connect-mongo  # MongoDB session store for Express

# Install dev dependencies
npm install -D typescript @types/node @types/express @types/cors
npm install -D ts-node nodemon
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install -D jest @types/jest ts-jest supertest @types/supertest

Monorepo Structure:

bast-admin-ui/                    # Monorepo root
├── apps/
│   ├── admin-ui/                 # React frontend (existing)
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   │
│   └── bff/                      # Node.js BFF (new)
│       ├── src/
│       │   ├── server.ts         # Express server entry
│       │   ├── app.ts            # App configuration
│       │   ├── trpc/
│       │   │   ├── router.ts    # Main tRPC router
│       │   │   ├── context.ts   # tRPC context
│       │   │   └── middleware.ts # Auth middleware
│       │   ├── services/
│       │   │   ├── fastapi-client.ts
│       │   │   ├── cache.ts
│       │   │   └── mongodb.ts
│       │   ├── routers/
│       │   │   ├── auth.router.ts
│       │   │   ├── users.router.ts
│       │   │   └── ontology.router.ts
│       │   ├── types/
│       │   │   └── index.ts
│       │   └── utils/
│       │       └── errors.ts
│       ├── tests/
│       │   ├── unit/
│       │   └── integration/
│       ├── tsconfig.json
│       ├── package.json
│       └── .env.example
│
├── package.json                  # Root workspace config
├── pnpm-workspace.yaml           # Workspace configuration
└── turbo.json                    # Turborepo config (optional)

Key Benefit: Types can be imported directly from BFF in admin-ui:

// apps/admin-ui/src/lib/trpc.ts
import type { AppRouter } from '../../bff/src/trpc/router';

Step 1.2: Configure TypeScript

File: tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

Step 1.3: Set Up Environment Variables

File: .env.example

# Server Configuration
PORT=4000
NODE_ENV=development

# FastAPI Services (existing)
FASTAPI_AUTH_URL=http://localhost:8000
FASTAPI_ONTOLOGY_URL=http://localhost:8001
FASTAPI_DATA_URL=http://localhost:8002

# Redis Cache
REDIS_URL=redis://localhost:6379
REDIS_TTL=3600

# MongoDB Atlas (for sessions)
MONGODB_URI=mongodb+srv://<user>:<password>@<cluster>.mongodb.net/bast_bff?retryWrites=true&w=majority
# For local development, use:
# MONGODB_URI=mongodb://localhost:27017/bast_bff

# JWT Configuration
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRES_IN=24h

# CORS
FRONTEND_URL=http://localhost:3000

Step 1.4: Create Basic Express Server

File: src/server.ts

import express from 'express';
import cors from 'cors';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { appRouter } from './trpc/router';
import { createContext } from './trpc/context';

const app = express();
const PORT = process.env.PORT || 4000;

// Middleware
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:3000',
  credentials: true,
}));
app.use(express.json());

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// tRPC endpoint
app.use('/trpc', createExpressMiddleware({
  router: appRouter,
  createContext,
}));

app.listen(PORT, () => {
  console.log(`🚀 BFF server running on http://localhost:${PORT}`);
  console.log(`📡 tRPC endpoint: http://localhost:${PORT}/trpc`);
});

Step 1.5: Set Up Basic tRPC Router

File: src/trpc/router.ts

import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.context<Context>().create();

export const router = t.router;
export const publicProcedure = t.procedure;

// Example router
export const appRouter = router({
  hello: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return { message: `Hello ${input.name}!` };
    }),
});

export type AppRouter = typeof appRouter;

File: src/trpc/context.ts

import { CreateExpressContextOptions } from '@trpc/server/adapters/express';

export interface Context {
  // Add context properties (user, request, etc.)
}

export function createContext({ req, res }: CreateExpressContextOptions): Context {
  return {
    // Initialize context
  };
}

Testing Phase 1

Test 1.1: Server Starts Successfully

npm run dev
# Expected: Server starts on port 4000
# Expected: Health check returns 200 OK

Test 1.2: tRPC Endpoint Responds

curl -X POST http://localhost:4000/trpc/hello \
  -H "Content-Type: application/json" \
  -d '{"name": "World"}'
# Expected: {"result":{"data":{"message":"Hello World!"}}}

Test 1.3: TypeScript Compiles

npm run build
# Expected: No compilation errors

✅ Phase 1 Complete Criteria: - [ ] Server starts without errors - [ ] Health check endpoint works - [ ] Basic tRPC endpoint responds - [ ] TypeScript compiles successfully - [ ] Project structure is organized


Phase 2: BFF Core Implementation

Goal: Implement core tRPC infrastructure with proper error handling and logging.

Step 2.1: Enhanced tRPC Setup with Error Handling

File: src/trpc/router.ts (updated)

import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
import type { Context } from './context';

const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        code: error.code,
        httpStatus: getHTTPStatusCodeFromError(error),
      },
    };
  },
});

// Middleware for logging
const loggingMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const duration = Date.now() - start;

  console.log(`${type.toUpperCase()} ${path} - ${duration}ms`);
  return result;
});

export const router = t.router;
export const publicProcedure = t.procedure.use(loggingMiddleware);
export const protectedProcedure = t.procedure.use(loggingMiddleware);

Step 2.2: Create FastAPI Client Service

File: src/services/fastapi-client.ts

import axios, { AxiosInstance, AxiosError } from 'axios';

export class FastAPIClient {
  private clients: Map<string, AxiosInstance> = new Map();

  constructor() {
    // Initialize clients for each FastAPI service
    this.clients.set('auth', axios.create({
      baseURL: process.env.FASTAPI_AUTH_URL || 'http://localhost:8000',
      timeout: 10000,
    }));

    this.clients.set('ontology', axios.create({
      baseURL: process.env.FASTAPI_ONTOLOGY_URL || 'http://localhost:8001',
      timeout: 10000,
    }));

    this.clients.set('data', axios.create({
      baseURL: process.env.FASTAPI_DATA_URL || 'http://localhost:8002',
      timeout: 10000,
    }));
  }

  async get(service: string, endpoint: string, config?: any) {
    const client = this.clients.get(service);
    if (!client) {
      throw new Error(`Unknown service: ${service}`);
    }

    try {
      const response = await client.get(endpoint, config);
      return response.data;
    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw this.handleError(error);
      }
      throw error;
    }
  }

  async post(service: string, endpoint: string, data?: any, config?: any) {
    const client = this.clients.get(service);
    if (!client) {
      throw new Error(`Unknown service: ${service}`);
    }

    try {
      const response = await client.post(endpoint, data, config);
      return response.data;
    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw this.handleError(error);
      }
      throw error;
    }
  }

  private handleError(error: AxiosError) {
    if (error.response) {
      // FastAPI returned an error response
      return new Error(`FastAPI Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
    } else if (error.request) {
      // Request made but no response
      return new Error(`FastAPI Unreachable: ${error.message}`);
    } else {
      // Something else happened
      return new Error(`Request Error: ${error.message}`);
    }
  }
}

export const fastAPIClient = new FastAPIClient();

Step 2.3: Add Request/Response Logging

File: src/utils/logger.ts

export const logger = {
  info: (message: string, meta?: any) => {
    console.log(`[INFO] ${message}`, meta || '');
  },
  error: (message: string, error?: any) => {
    console.error(`[ERROR] ${message}`, error || '');
  },
  warn: (message: string, meta?: any) => {
    console.warn(`[WARN] ${message}`, meta || '');
  },
};

Testing Phase 2

Test 2.1: FastAPI Client Connects

// tests/unit/fastapi-client.test.ts
import { fastAPIClient } from '../../src/services/fastapi-client';

describe('FastAPIClient', () => {
  it('should create clients for all services', () => {
    // Test client initialization
  });

  it('should handle FastAPI errors gracefully', async () => {
    // Test error handling
  });
});

Test 2.2: Error Handling Works

# Test with non-existent FastAPI service
curl http://localhost:4000/trpc/testEndpoint
# Expected: Proper error response with status code

✅ Phase 2 Complete Criteria: - [ ] FastAPI client service implemented - [ ] Error handling works correctly - [ ] Logging middleware functional - [ ] Unit tests pass


Phase 3: Authentication & Session Management

Goal: Implement authentication flow and session management in the BFF.

Step 3.1: Install Auth Dependencies

npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs
npm install express-session connect-mongo
npm install -D @types/express-session

Step 3.1a: Set Up MongoDB Connection

File: src/services/mongodb.ts

import mongoose from 'mongoose';

export async function connectMongoDB() {
  const uri = process.env.MONGODB_URI;

  if (!uri) {
    throw new Error('MONGODB_URI environment variable is not set');
  }

  try {
    await mongoose.connect(uri);
    console.log('✅ MongoDB connected');
  } catch (error) {
    console.error('❌ MongoDB connection error:', error);
    throw error;
  }
}

// Handle connection events
mongoose.connection.on('error', (err) => {
  console.error('MongoDB connection error:', err);
});

mongoose.connection.on('disconnected', () => {
  console.warn('MongoDB disconnected');
});

// Graceful shutdown
process.on('SIGINT', async () => {
  await mongoose.connection.close();
  console.log('MongoDB connection closed');
  process.exit(0);
});

File: src/server.ts (updated)

import express from 'express';
import cors from 'cors';
import session from 'express-session';
import MongoStore from 'connect-mongo';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { appRouter } from './trpc/router';
import { createContext } from './trpc/context';
import { connectMongoDB } from './services/mongodb';

const app = express();
const PORT = process.env.PORT || 4000;

// Connect to MongoDB
connectMongoDB().catch(console.error);

// Session configuration with MongoDB store
app.use(session({
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI,
    dbName: 'bast_bff',
    collectionName: 'sessions',
    ttl: 24 * 60 * 60, // 24 hours
  }),
  secret: process.env.SESSION_SECRET || 'change-me-in-production',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
}));

// ... rest of server setup

Step 3.2: Create Auth Middleware

File: src/trpc/middleware.ts

import { TRPCError } from '@trpc/server';
import { t } from './router';

export const isAuthenticated = t.middleware(async ({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'You must be logged in to access this resource',
    });
  }
  return next({
    ctx: {
      ...ctx,
      user: ctx.user, // Now user is guaranteed to be defined
    },
  });
});

Step 3.3: Update Context with User

File: src/trpc/context.ts (updated)

import { CreateExpressContextOptions } from '@trpc/server/adapters/express';
import { verifyToken } from '../utils/auth';

export interface User {
  id: string;
  email: string;
  organization: string;
}

export interface Context {
  user?: User;
  req: any;
  res: any;
}

export async function createContext({ req, res }: CreateExpressContextOptions): Promise<Context> {
  // Extract token from Authorization header or cookie
  const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token;

  let user: User | undefined;

  if (token) {
    try {
      user = await verifyToken(token);
    } catch (error) {
      // Token invalid, but don't throw - let endpoints decide
      console.warn('Invalid token:', error);
    }
  }

  return {
    user,
    req,
    res,
  };
}

Step 3.4: Create Auth Router

File: src/routers/auth.router.ts

import { router, publicProcedure, protectedProcedure } from '../trpc/router';
import { z } from 'zod';
import { fastAPIClient } from '../services/fastapi-client';
import { generateToken } from '../utils/auth';

export const authRouter = router({
  login: publicProcedure
    .input(z.object({
      email: z.string().email(),
      password: z.string().min(6),
    }))
    .mutation(async ({ input }) => {
      // Call FastAPI auth service
      const response = await fastAPIClient.post('auth', '/login', {
        email: input.email,
        password: input.password,
      });

      // Generate JWT token
      const token = generateToken({
        id: response.user.id,
        email: response.user.email,
        organization: response.user.organization,
      });

      return {
        token,
        user: response.user,
      };
    }),

  me: protectedProcedure
    .query(async ({ ctx }) => {
      // Return current user from context
      return ctx.user;
    }),

  logout: protectedProcedure
    .mutation(async ({ ctx }) => {
      // Invalidate token (add to blacklist if needed)
      return { success: true };
    }),
});

Step 3.5: Update Main Router

File: src/trpc/router.ts (updated)

import { router } from './router';
import { authRouter } from '../routers/auth.router';

export const appRouter = router({
  auth: authRouter,
  // Add other routers here
});

export type AppRouter = typeof appRouter;

Testing Phase 3

Test 3.1: Login Endpoint

curl -X POST http://localhost:4000/trpc/auth.login \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "password123"}'
# Expected: Returns token and user data

Test 3.2: Protected Endpoint Without Auth

curl http://localhost:4000/trpc/auth.me
# Expected: 401 Unauthorized error

Test 3.3: Protected Endpoint With Auth

curl http://localhost:4000/trpc/auth.me \
  -H "Authorization: Bearer <token>"
# Expected: Returns user data

✅ Phase 3 Complete Criteria: - [ ] Login endpoint works - [ ] JWT token generation works - [ ] Protected endpoints require authentication - [ ] User context available in procedures - [ ] Integration tests pass


Phase 4: First Service Integration

Goal: Integrate with one existing FastAPI service (e.g., Users service) to establish the pattern.

Step 4.1: Create Users Router

File: src/routers/users.router.ts

import { router, protectedProcedure } from '../trpc/router';
import { z } from 'zod';
import { fastAPIClient } from '../services/fastapi-client';

export const usersRouter = router({
  list: protectedProcedure
    .input(z.object({
      organization: z.string().optional(),
      limit: z.number().min(1).max(100).default(10),
      offset: z.number().min(0).default(0),
    }))
    .query(async ({ input, ctx }) => {
      // Call FastAPI users service
      const response = await fastAPIClient.get('auth', '/users', {
        params: {
          organization: input.organization || ctx.user?.organization,
          limit: input.limit,
          offset: input.offset,
        },
        headers: {
          Authorization: `Bearer ${ctx.req.headers.authorization}`,
        },
      });

      // Shape response for frontend
      return {
        users: response.users,
        total: response.total,
        limit: input.limit,
        offset: input.offset,
      };
    }),

  getById: protectedProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ input, ctx }) => {
      const user = await fastAPIClient.get('auth', `/users/${input.userId}`, {
        headers: {
          Authorization: `Bearer ${ctx.req.headers.authorization}`,
        },
      });

      return user;
    }),

  update: protectedProcedure
    .input(z.object({
      userId: z.string(),
      data: z.object({
        email: z.string().email().optional(),
        name: z.string().optional(),
      }),
    }))
    .mutation(async ({ input, ctx }) => {
      const updated = await fastAPIClient.post(
        'auth',
        `/users/${input.userId}`,
        input.data,
        {
          headers: {
            Authorization: `Bearer ${ctx.req.headers.authorization}`,
          },
        }
      );

      return updated;
    }),
});

Step 4.2: Update Main Router

File: src/trpc/router.ts (updated)

import { router } from './router';
import { authRouter } from '../routers/auth.router';
import { usersRouter } from '../routers/users.router';

export const appRouter = router({
  auth: authRouter,
  users: usersRouter,
});

export type AppRouter = typeof appRouter;

Testing Phase 4

Test 4.1: List Users

curl http://localhost:4000/trpc/users.list \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"limit": 10, "offset": 0}'
# Expected: Returns paginated user list

Test 4.2: Get User By ID

curl http://localhost:4000/trpc/users.getById \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"userId": "123"}'
# Expected: Returns user data

Test 4.3: Update User

curl -X POST http://localhost:4000/trpc/users.update \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"userId": "123", "data": {"email": "new@example.com"}}'
# Expected: Returns updated user

✅ Phase 4 Complete Criteria: - [ ] Users router implemented - [ ] CRUD operations work - [ ] Response shaping for frontend - [ ] Error handling works - [ ] Integration tests pass


Phase 5: Request Aggregation & Response Shaping

Goal: Implement request aggregation for complex views that need data from multiple services.

Step 5.1: Create Aggregated Router

File: src/routers/dashboard.router.ts

import { router, protectedProcedure } from '../trpc/router';
import { fastAPIClient } from '../services/fastapi-client';

export const dashboardRouter = router({
  getOverview: protectedProcedure
    .query(async ({ ctx }) => {
      // Aggregate data from multiple FastAPI services in parallel
      const [users, ontology, governance] = await Promise.all([
        fastAPIClient.get('auth', '/users/stats', {
          headers: { Authorization: `Bearer ${ctx.req.headers.authorization}` },
        }),
        fastAPIClient.get('ontology', '/stats', {
          headers: { Authorization: `Bearer ${ctx.req.headers.authorization}` },
        }),
        fastAPIClient.get('data', '/governance/stats', {
          headers: { Authorization: `Bearer ${ctx.req.headers.authorization}` },
        }),
      ]);

      // Shape response for frontend dashboard
      return {
        users: {
          total: users.total,
          active: users.active,
        },
        ontology: {
          entities: ontology.entityCount,
          relationships: ontology.relationshipCount,
        },
        governance: {
          policies: governance.policyCount,
          violations: governance.violationCount,
        },
      };
    }),

  getUserDashboard: protectedProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ input, ctx }) => {
      // Fetch user data and related data in parallel
      const [user, permissions, activity] = await Promise.all([
        fastAPIClient.get('auth', `/users/${input.userId}`, {
          headers: { Authorization: `Bearer ${ctx.req.headers.authorization}` },
        }),
        fastAPIClient.get('auth', `/users/${input.userId}/permissions`, {
          headers: { Authorization: `Bearer ${ctx.req.headers.authorization}` },
        }),
        fastAPIClient.get('data', `/users/${input.userId}/activity`, {
          headers: { Authorization: `Bearer ${ctx.req.headers.authorization}` },
        }),
      ]);

      return {
        user,
        permissions,
        recentActivity: activity.items.slice(0, 10),
      };
    }),
});

Step 5.2: Add Response Transformation Utilities

File: src/utils/transformers.ts

export function transformUser(user: any) {
  return {
    id: user.userid || user.id,
    email: user.useremail || user.email,
    name: user.name,
    organization: user.defaultOrganization || user.organization,
    // Add any other transformations
  };
}

export function transformEntity(entity: any) {
  return {
    id: entity.id,
    name: entity.name,
    type: entity.type,
    // Shape for frontend needs
  };
}

Testing Phase 5

Test 5.1: Aggregated Dashboard Endpoint

curl http://localhost:4000/trpc/dashboard.getOverview \
  -H "Authorization: Bearer <token>"
# Expected: Returns aggregated data from multiple services

Test 5.2: Performance Check

# Measure response time
time curl http://localhost:4000/trpc/dashboard.getOverview \
  -H "Authorization: Bearer <token>"
# Expected: Response time < 500ms (with parallel requests)

✅ Phase 5 Complete Criteria: - [ ] Request aggregation works - [ ] Parallel requests implemented - [ ] Response shaping utilities created - [ ] Performance is acceptable - [ ] Tests pass


Phase 6: Caching Layer

Goal: Implement Redis caching to reduce load on FastAPI services.

Step 6.1: Install Redis Client

npm install redis
npm install -D @types/redis

Step 6.2: Create Cache Service

File: src/services/cache.ts

import { createClient } from 'redis';

class CacheService {
  private client: ReturnType<typeof createClient>;
  private connected = false;

  async connect() {
    this.client = createClient({
      url: process.env.REDIS_URL || 'redis://localhost:6379',
    });

    this.client.on('error', (err) => {
      console.error('Redis Client Error', err);
      this.connected = false;
    });

    await this.client.connect();
    this.connected = true;
    console.log('✅ Redis connected');
  }

  async get<T>(key: string): Promise<T | null> {
    if (!this.connected) return null;

    try {
      const value = await this.client.get(key);
      return value ? JSON.parse(value) : null;
    } catch (error) {
      console.error('Cache get error:', error);
      return null;
    }
  }

  async set(key: string, value: any, ttl: number = 3600): Promise<void> {
    if (!this.connected) return;

    try {
      await this.client.setEx(key, ttl, JSON.stringify(value));
    } catch (error) {
      console.error('Cache set error:', error);
    }
  }

  async invalidate(pattern: string): Promise<void> {
    if (!this.connected) return;

    try {
      const keys = await this.client.keys(pattern);
      if (keys.length > 0) {
        await this.client.del(keys);
      }
    } catch (error) {
      console.error('Cache invalidate error:', error);
    }
  }
}

export const cache = new CacheService();

Step 6.3: Add Caching Middleware

File: src/trpc/middleware.ts (updated)

import { cache } from '../services/cache';

export const withCache = (ttl: number = 3600) => {
  return t.middleware(async ({ path, input, next }) => {
    // Generate cache key
    const cacheKey = `trpc:${path}:${JSON.stringify(input)}`;

    // Try to get from cache
    const cached = await cache.get(cacheKey);
    if (cached) {
      return cached;
    }

    // Execute procedure
    const result = await next();

    // Cache result
    await cache.set(cacheKey, result, ttl);

    return result;
  });
};

Step 6.4: Apply Caching to Routers

File: src/routers/users.router.ts (updated)

import { withCache } from '../trpc/middleware';

export const usersRouter = router({
  list: protectedProcedure
    .use(withCache(300)) // Cache for 5 minutes
    .input(z.object({...}))
    .query(async ({ input, ctx }) => {
      // ... existing implementation
    }),
});

Testing Phase 6

Test 6.1: Cache Hit

# First request
curl http://localhost:4000/trpc/users.list \
  -H "Authorization: Bearer <token>"
# Expected: Response from FastAPI

# Second request (should be cached)
curl http://localhost:4000/trpc/users.list \
  -H "Authorization: Bearer <token>"
# Expected: Faster response, from cache

Test 6.2: Cache Invalidation

# Update user
curl -X POST http://localhost:4000/trpc/users.update \
  -H "Authorization: Bearer <token>" \
  -d '{"userId": "123", "data": {...}}'

# Cache should be invalidated
# Next list request should fetch fresh data

✅ Phase 6 Complete Criteria: - [ ] Redis connection works - [ ] Cache get/set works - [ ] Cache middleware functional - [ ] Cache invalidation works - [ ] Performance improved


Phase 7: Real-time Features (WebSockets)

Goal: Add WebSocket support for real-time updates.

Step 7.1: Install WebSocket Dependencies

npm install ws
npm install -D @types/ws

Step 7.2: Create WebSocket Server

File: src/websocket/server.ts

import { WebSocketServer } from 'ws';
import { applyWSSHandler } from '@trpc/server/adapters/ws';
import { appRouter } from '../trpc/router';
import { createContext } from '../trpc/context';

export function createWebSocketServer(server: any) {
  const wss = new WebSocketServer({ server });

  const handler = applyWSSHandler({
    wss,
    router: appRouter,
    createContext: async (opts) => {
      // Create context for WebSocket connection
      return createContext(opts as any);
    },
  });

  console.log('✅ WebSocket server ready');

  return {
    wss,
    handler,
  };
}

Step 7.3: Add Subscription Router

File: src/routers/notifications.router.ts

import { router, protectedProcedure } from '../trpc/router';
import { observable } from '@trpc/server/observable';

export const notificationsRouter = router({
  subscribe: protectedProcedure
    .subscription(async ({ ctx }) => {
      return observable<{ message: string; timestamp: Date }>((emit) => {
        // Set up subscription to FastAPI WebSocket or polling
        const interval = setInterval(async () => {
          // Poll for new notifications
          const notifications = await fastAPIClient.get(
            'data',
            '/notifications',
            { headers: { Authorization: `Bearer ${ctx.req.headers.authorization}` } }
          );

          notifications.forEach((notification: any) => {
            emit.next({
              message: notification.message,
              timestamp: new Date(notification.timestamp),
            });
          });
        }, 5000); // Poll every 5 seconds

        return () => {
          clearInterval(interval);
        };
      });
    }),
});

Testing Phase 7

Test 7.1: WebSocket Connection

// Use WebSocket client to connect
const ws = new WebSocket('ws://localhost:4000');
ws.on('open', () => {
  console.log('Connected');
});

Test 7.2: Subscription Works

// Test subscription endpoint
// Expected: Receives real-time updates

✅ Phase 7 Complete Criteria: - [ ] WebSocket server starts - [ ] Subscriptions work - [ ] Real-time updates delivered - [ ] Connection cleanup works


Phase 8: Frontend Integration

Goal: Integrate tRPC client into React frontend.

Step 8.1: Install Frontend Dependencies

# From monorepo root
pnpm add @trpc/client@latest @trpc/react-query@latest --filter admin-ui
pnpm add @tanstack/react-query@latest --filter admin-ui

# Or from apps/admin-ui directory
cd apps/admin-ui
npm install @trpc/client@latest @trpc/react-query@latest
npm install @tanstack/react-query@latest

Step 8.2: Create tRPC Client

File: apps/admin-ui/src/lib/trpc.ts

import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink } from '@trpc/client';
// Direct import from BFF in monorepo - types are shared!
import type { AppRouter } from '../../bff/src/trpc/router';

export const trpc = createTRPCReact<AppRouter>();

export const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: import.meta.env.VITE_BFF_URL || 'http://localhost:4000/trpc',
      headers: () => {
        const token = localStorage.getItem('token');
        return {
          authorization: token ? `Bearer ${token}` : '',
        };
      },
    }),
  ],
});

Step 8.3: Set Up Provider

File: apps/admin-ui/src/providers/TRPCProvider.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { trpc, trpcClient } from '../lib/trpc';
import { useState } from 'react';

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 60 * 1000, // 5 minutes
        refetchOnWindowFocus: false,
      },
    },
  }));

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

Step 8.4: Update App.tsx

File: apps/admin-ui/src/App.tsx (updated)

import { TRPCProvider } from './providers/TRPCProvider';

export default function App() {
  return (
    <TRPCProvider>
      {/* Existing app content */}
    </TRPCProvider>
  );
}

Step 8.5: Use tRPC in Components

File: apps/admin-ui/src/components/UsersList.tsx

import { trpc } from '../lib/trpc';

export function UsersList() {
  const { data, isLoading, error } = trpc.users.list.useQuery({
    limit: 10,
    offset: 0,
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {data?.users.map((user) => (
        <div key={user.id}>{user.email}</div>
      ))}
    </div>
  );
}

Testing Phase 8

Test 8.1: Frontend Connects to BFF

# Start frontend
npm run dev

# Navigate to page using tRPC
# Expected: Data loads from BFF

Test 8.2: Type Safety

// TypeScript should autocomplete and type-check
trpc.users.list.useQuery({
  limit: 10, // ✅ Type-safe
  // invalidField: 'test', // ❌ TypeScript error
});

✅ Phase 8 Complete Criteria: - [ ] Frontend connects to BFF - [ ] tRPC hooks work - [ ] Type safety works - [ ] Data loads correctly - [ ] Error handling works


Phase 9: Production Readiness

Goal: Prepare BFF for production deployment.

Step 9.1: Add Rate Limiting

npm install express-rate-limit

File: src/middleware/rateLimit.ts

import rateLimit from 'express-rate-limit';

export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again later.',
});

Step 9.2: Add Request Validation

File: src/middleware/validation.ts

import { z } from 'zod';

export function validateInput<T>(schema: z.ZodSchema<T>) {
  return (input: unknown): T => {
    return schema.parse(input);
  };
}

Step 9.3: Add Monitoring & Logging

npm install winston

File: src/utils/logger.ts (updated)

import winston from 'winston';

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

Step 9.4: Add Health Checks

File: src/server.ts (updated)

app.get('/health', async (req, res) => {
  const health = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    services: {
      mongodb: await checkMongoDB(),
      redis: await checkRedis(),
      fastapi: await checkFastAPI(),
    },
  };

  const allHealthy = Object.values(health.services).every(s => s === 'ok');
  res.status(allHealthy ? 200 : 503).json(health);
});

async function checkMongoDB(): Promise<'ok' | 'error'> {
  try {
    const mongoose = require('mongoose');
    return mongoose.connection.readyState === 1 ? 'ok' : 'error';
  } catch {
    return 'error';
  }
}

async function checkRedis(): Promise<'ok' | 'error'> {
  // Implementation for Redis health check
  return 'ok';
}

async function checkFastAPI(): Promise<'ok' | 'error'> {
  // Implementation for FastAPI health check
  return 'ok';
}

Step 9.5: Docker Configuration

File: Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 4000

CMD ["node", "dist/server.js"]

File: docker-compose.yml

version: '3.8'

services:
  bff:
    build: .
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379
      # Use MongoDB Atlas connection string in production
      # For local development, use: MONGODB_URI=mongodb://mongodb:27017/bast_bff
      - MONGODB_URI=${MONGODB_URI}
      - SESSION_SECRET=${SESSION_SECRET}
    depends_on:
      - redis
      - mongodb  # Only for local development

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  # MongoDB for local development only
  # In production, use MongoDB Atlas (cloud)
  mongodb:
    image: mongo:7
    environment:
      - MONGO_INITDB_DATABASE=bast_bff
    ports:
      - "27017:27017"
    volumes:
      - mongodb_data:/data/db

volumes:
  mongodb_data:

Testing Phase 9

Test 9.1: Rate Limiting

# Send 101 requests rapidly
for i in {1..101}; do
  curl http://localhost:4000/trpc/users.list
done
# Expected: 101st request returns 429 Too Many Requests

Test 9.2: Health Check

curl http://localhost:4000/health
# Expected: Returns health status of all services including MongoDB
# Expected response includes: mongodb, redis, fastapi status

Test 9.3: Docker Build

docker build -t bast-bff .
docker run -p 4000:4000 bast-bff
# Expected: Container starts and BFF is accessible

✅ Phase 9 Complete Criteria: - [ ] Rate limiting works - [ ] Monitoring/logging configured - [ ] Health checks work - [ ] Docker configuration works - [ ] Production environment variables set


Testing Strategy

Unit Tests

Location: tests/unit/

Test individual functions and services: - FastAPI client methods - Cache service methods - Auth utilities - Transformers

Example:

// tests/unit/cache.test.ts
import { cache } from '../../src/services/cache';

describe('CacheService', () => {
  beforeEach(async () => {
    await cache.connect();
  });

  it('should set and get values', async () => {
    await cache.set('test-key', { data: 'test' });
    const value = await cache.get('test-key');
    expect(value).toEqual({ data: 'test' });
  });
});

Integration Tests

Location: tests/integration/

Test BFF endpoints with mocked FastAPI services: - Auth flow - User CRUD operations - Request aggregation - Caching behavior

Example:

// tests/integration/users.test.ts
import request from 'supertest';
import { app } from '../../src/server';

describe('Users API', () => {
  it('should list users', async () => {
    const response = await request(app)
      .post('/trpc/users.list')
      .set('Authorization', 'Bearer valid-token')
      .send({ limit: 10, offset: 0 });

    expect(response.status).toBe(200);
    expect(response.body.result.data.users).toBeDefined();
  });
});

End-to-End Tests

Location: tests/e2e/

Test full flow from frontend to BFF to FastAPI: - User login flow - Data fetching - Real-time updates

Performance Tests

Tools: Artillery, k6, or Apache Bench

# Example: Load test
artillery quick --count 100 --num 10 http://localhost:4000/trpc/users.list

Test Checklist

  • Unit tests for all services
  • Integration tests for all routers
  • E2E tests for critical flows
  • Performance tests
  • Error handling tests
  • Authentication tests
  • Caching tests

Rollback Plan

If Issues Occur

  1. Immediate Rollback
  2. Switch frontend back to direct FastAPI calls
  3. Keep BFF running but route traffic around it
  4. Investigate issues in parallel

  5. Partial Rollback

  6. Route only specific endpoints through BFF
  7. Gradually migrate back to direct calls

  8. Data Recovery

  9. Ensure FastAPI services are unchanged
  10. No data loss expected (BFF is stateless)

Monitoring Points

  • Response times
  • Error rates
  • Cache hit rates
  • FastAPI service health
  • BFF server health

Success Metrics

Performance

  • ✅ BFF response time < 100ms (with cache)
  • ✅ BFF response time < 500ms (without cache)
  • ✅ 99.9% uptime
  • ✅ Cache hit rate > 70%

Functionality

  • ✅ All existing features work
  • ✅ Type safety end-to-end
  • ✅ Real-time features functional
  • ✅ Authentication works

Developer Experience

  • ✅ Type autocomplete works
  • ✅ Easy to add new endpoints
  • ✅ Clear error messages
  • ✅ Good documentation

Timeline Estimate

Phase Duration Dependencies
Phase 1: Setup 2-3 days None
Phase 2: Core 3-4 days Phase 1
Phase 3: Auth 3-4 days Phase 2
Phase 4: First Service 2-3 days Phase 3
Phase 5: Aggregation 2-3 days Phase 4
Phase 6: Caching 2-3 days Phase 4
Phase 7: WebSockets 3-4 days Phase 4
Phase 8: Frontend 4-5 days Phase 4
Phase 9: Production 3-4 days All phases

Total Estimated Time: 4-6 weeks (with one developer)


Next Steps

  1. Review this plan with the team
  2. Set up development environment
  3. Start with Phase 1
  4. Create GitHub issues for each phase
  5. Set up CI/CD pipeline
  6. Begin implementation

Document created for Bast Admin UI BFF Implementation