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¶
- Overview
- Prerequisites
- Phase 1: Project Setup & Infrastructure
- Phase 2: BFF Core Implementation
- Phase 3: Authentication & Session Management
- Phase 4: First Service Integration
- Phase 5: Request Aggregation & Response Shaping
- Phase 6: Caching Layer
- Phase 7: Real-time Features (WebSockets)
- Phase 8: Frontend Integration
- Phase 9: Production Readiness
- Testing Strategy
- 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):
- Create MongoDB Atlas Account
- Go to MongoDB Atlas
-
Sign up for a free account (M0 cluster is free)
-
Create a Cluster
- Choose a cloud provider (AWS, Azure, GCP)
- Select a region close to your deployment
-
Create an M0 (Free) cluster for development
-
Configure Database Access
- Go to "Database Access"
- Create a database user with username/password
-
Set appropriate permissions (read/write)
-
Configure Network Access
- Go to "Network Access"
- Add IP address:
0.0.0.0/0for development (or specific IPs for production) -
Or use "Allow Access from Anywhere" for testing
-
Get Connection String
- Go to "Database" → "Connect"
- Choose "Connect your application"
- Copy the connection string
- Format:
mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority -
Replace
<username>,<password>, and<dbname>with your values -
Set Environment Variable
For Local Development:
You can use MongoDB locally with Docker:
Then use:
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:
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
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
✅ 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
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¶
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¶
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
✅ 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
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¶
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¶
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
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¶
- Immediate Rollback
- Switch frontend back to direct FastAPI calls
- Keep BFF running but route traffic around it
-
Investigate issues in parallel
-
Partial Rollback
- Route only specific endpoints through BFF
-
Gradually migrate back to direct calls
-
Data Recovery
- Ensure FastAPI services are unchanged
- 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¶
- Review this plan with the team
- Set up development environment
- Start with Phase 1
- Create GitHub issues for each phase
- Set up CI/CD pipeline
- Begin implementation
Document created for Bast Admin UI BFF Implementation