TypeScript tRPC Gateway API โ
A type-safe API gateway built on tRPC 11 + Express 5, providing unified end-to-end type-safe interface layer for frontend applications.
API Documentation: https://halolight-bff.h7ml.cn
GitHub: https://github.com/halolight/halolight-bff
Features โ
- ๐ฏ End-to-End Type Safety - tRPC provides complete type inference from server to client with zero runtime overhead
- ๐ JWT Dual Token Auth - Access Token + Refresh Token auto-renewal with RBAC permission control
- ๐ก Service Gateway Aggregation - Unified aggregation of multiple backend services (Python/Java/Go/Bun) with automatic failover
- โ Zod Data Validation - Automatic input validation, type-safe with detailed error messages
- ๐ SuperJSON Serialization - Automatic handling of Date, Map, Set, BigInt, RegExp and other complex types
- ๐ญ Request Batching - Automatic batch processing of multiple requests to reduce network overhead
- ๐ Distributed Tracing - Automatic Trace ID propagation with complete request chain logging
- ๐ณ Docker Support - Containerized deployment with production-grade configuration
Tech Stack โ
| Technology | Version | Description |
|---|---|---|
| TypeScript | 5.9 | Programming Language |
| tRPC | 11 | RPC Framework |
| Zod | - | Data Validation |
| Express | 5 | Web Server |
| SuperJSON | - | Serialization |
| JWT | - | Authentication |
| Pino | - | Logging System |
Quick Start โ
Requirements โ
- Node.js >= 20.0
- pnpm >= 8.0
- At least one backend service (Python/Java/Go/Bun)
Installation โ
# Clone repository
git clone https://github.com/halolight/halolight-bff.git
cd halolight-bff
# Install dependencies
pnpm installEnvironment Variables โ
cp .env.example .env# Server configuration
PORT=3002
HOST=0.0.0.0
NODE_ENV=development
# JWT secret (must change in production)
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d
# CORS configuration
CORS_ORIGIN=*
# Log level
LOG_LEVEL=info
# Backend service registry (configure at least one)
HALOLIGHT_API_PYTHON_URL=http://localhost:8000
HALOLIGHT_API_BUN_URL=http://localhost:3000
HALOLIGHT_API_JAVA_URL=http://localhost:8080
HALOLIGHT_API_NESTJS_URL=http://localhost:3001
HALOLIGHT_API_NODE_URL=http://localhost:3003
HALOLIGHT_API_GO_URL=http://localhost:8081Database Initialization โ
No database required (API gateway does not directly access database).
Start Service โ
# Development mode (hot reload)
pnpm dev
# Production mode
pnpm build
pnpm startVisit http://localhost:3002
Project Structure โ
halolight-bff/
โโโ src/
โ โโโ index.ts # Application entry
โ โโโ server.ts # Express server + tRPC adapter
โ โโโ trpc.ts # tRPC instance and procedure definitions
โ โโโ context.ts # Context creation (user, tracing, services)
โ โโโ routers/
โ โ โโโ index.ts # Root router (combining all modules)
โ โ โโโ auth.ts # Authentication module (8 endpoints)
โ โ โโโ users.ts # User management (8 endpoints)
โ โ โโโ dashboard.ts # Dashboard statistics (9 endpoints)
โ โ โโโ permissions.ts # Permission management (7 endpoints)
โ โ โโโ roles.ts # Role management (8 endpoints)
โ โ โโโ teams.ts # Team management (9 endpoints)
โ โ โโโ folders.ts # Folder management (8 endpoints)
โ โ โโโ files.ts # File management (9 endpoints)
โ โ โโโ documents.ts # Document management (10 endpoints)
โ โ โโโ calendar.ts # Calendar events (10 endpoints)
โ โ โโโ notifications.ts # Notifications (7 endpoints)
โ โ โโโ messages.ts # Messaging/chat (9 endpoints)
โ โโโ middleware/
โ โ โโโ auth.ts # JWT authentication/authorization middleware
โ โโโ services/
โ โ โโโ httpClient.ts # HTTP client (backend communication)
โ โ โโโ serviceRegistry.ts # Backend service registry
โ โโโ schemas/
โ โโโ index.ts # Schema exports
โ โโโ common.ts # Common Zod schemas (pagination, sorting, response)
โโโ .env.example # Environment variables template
โโโ .github/workflows/ # CI/CD configuration
โโโ Dockerfile # Docker image build
โโโ docker-compose.yml # Docker Compose configuration
โโโ package.json # Dependencies configuration
โโโ tsconfig.json # TypeScript configurationAPI Modules โ
HaloLight BFF provides 12 core business modules covering 100+ tRPC endpoints:
| Module | Endpoints | Description |
|---|---|---|
| auth | 8 | Login, register, token refresh, logout, password management |
| users | 8 | User CRUD, role/status management, profile |
| dashboard | 9 | Statistics, visit trends, sales data, tasks, calendar |
| permissions | 7 | Permission CRUD, tree structure, module permissions, batch operations |
| roles | 8 | Role CRUD, permission assignment, user association |
| teams | 9 | Team CRUD, member management, invitations, permissions |
| folders | 8 | Folder CRUD, tree structure, move, breadcrumb |
| files | 9 | File CRUD, upload, download, move, copy, share |
| documents | 10 | Document CRUD, version control, collaboration, sharing |
| calendar | 10 | Event CRUD, attendee management, RSVP, reminders |
| notifications | 7 | Notification list, unread count, mark read, batch delete |
| messages | 9 | Conversation management, message CRUD, send, read status |
Authentication Endpoints โ
| Procedure | Type | Description | Permission |
|---|---|---|---|
auth.login | mutation | User login | Public |
auth.register | mutation | User registration | Public |
auth.refresh | mutation | Refresh token | Public |
auth.logout | mutation | Logout | Authenticated |
auth.forgotPassword | mutation | Forgot password | Public |
auth.resetPassword | mutation | Reset password | Public |
auth.verifyEmail | mutation | Verify email | Public |
auth.changePassword | mutation | Change password | Authenticated |
User Management Endpoints โ
| Procedure | Type | Description | Permission |
|---|---|---|---|
users.list | query | Get user list | users:view |
users.byId | query | Get user details | users:view |
users.me | query | Get current user | Authenticated |
users.create | mutation | Create user | users:create |
users.update | mutation | Update user | users:update |
users.delete | mutation | Delete user | users:delete |
users.updateRole | mutation | Update user role | users:update |
users.updateStatus | mutation | Update user status | users:update |
Complete Endpoint List โ
Dashboard - 9 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
dashboard.getStats | query | Statistics (users, documents, files, tasks) |
dashboard.getVisits | query | Visit trends (7/30 days) |
dashboard.getSales | query | Sales data (line chart) |
dashboard.getPieData | query | Pie chart data (category distribution) |
dashboard.getTasks | query | Todo task list |
dashboard.getCalendar | query | Today's calendar |
dashboard.getActivities | query | Recent activities |
dashboard.getNotifications | query | Latest notifications |
dashboard.getProgress | query | Project progress |
Permissions - 7 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
permissions.list | query | Get permission list |
permissions.tree | query | Get permission tree |
permissions.byId | query | Get permission details |
permissions.create | mutation | Create permission |
permissions.update | mutation | Update permission |
permissions.delete | mutation | Delete permission |
permissions.modules | query | Get permission modules |
Roles - 8 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
roles.list | query | Get role list |
roles.byId | query | Get role details |
roles.create | mutation | Create role |
roles.update | mutation | Update role |
roles.delete | mutation | Delete role |
roles.assignPermissions | mutation | Assign permissions |
roles.removePermissions | mutation | Remove permissions |
roles.users | query | Get users in role |
Teams - 9 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
teams.list | query | Get team list |
teams.byId | query | Get team details |
teams.create | mutation | Create team |
teams.update | mutation | Update team |
teams.delete | mutation | Delete team |
teams.addMember | mutation | Add member |
teams.removeMember | mutation | Remove member |
teams.updateMemberRole | mutation | Update member role |
teams.members | query | Get team members |
Folders - 8 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
folders.list | query | Get folder list |
folders.tree | query | Get folder tree |
folders.byId | query | Get folder details |
folders.create | mutation | Create folder |
folders.update | mutation | Update folder |
folders.delete | mutation | Delete folder |
folders.move | mutation | Move folder |
folders.breadcrumb | query | Get breadcrumb path |
Files - 9 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
files.list | query | Get file list |
files.byId | query | Get file details |
files.upload | mutation | Upload file |
files.update | mutation | Update file info |
files.delete | mutation | Delete file |
files.move | mutation | Move file |
files.copy | mutation | Copy file |
files.download | query | Get download link |
files.share | mutation | Share file |
Documents - 10 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
documents.list | query | Get document list |
documents.byId | query | Get document details |
documents.create | mutation | Create document |
documents.update | mutation | Update document |
documents.delete | mutation | Delete document |
documents.versions | query | Get version history |
documents.restore | mutation | Restore version |
documents.share | mutation | Share document |
documents.unshare | mutation | Unshare document |
documents.collaborators | query | Get collaborators |
Calendar - 10 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
calendar.events | query | Get event list |
calendar.byId | query | Get event details |
calendar.create | mutation | Create event |
calendar.update | mutation | Update event |
calendar.delete | mutation | Delete event |
calendar.addAttendee | mutation | Add attendee |
calendar.removeAttendee | mutation | Remove attendee |
calendar.rsvp | mutation | RSVP response |
calendar.setReminder | mutation | Set reminder |
calendar.byMonth | query | Get events by month |
Notifications - 7 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
notifications.list | query | Get notification list |
notifications.unreadCount | query | Get unread count |
notifications.markRead | mutation | Mark as read |
notifications.markAllRead | mutation | Mark all as read |
notifications.delete | mutation | Delete notification |
notifications.deleteAll | mutation | Delete all |
notifications.preferences | query | Get notification preferences |
Messages - 9 Endpoints โ
| Procedure | Type | Description |
|---|---|---|
messages.conversations | query | Get conversation list |
messages.byConversation | query | Get conversation messages |
messages.send | mutation | Send message |
messages.markRead | mutation | Mark as read |
messages.delete | mutation | Delete message |
messages.createConversation | mutation | Create conversation |
messages.deleteConversation | mutation | Delete conversation |
messages.search | query | Search messages |
messages.unreadCount | query | Get unread count |
Core Concepts โ
tRPC Procedures โ
tRPC provides three procedure types:
// Public endpoint - no authentication required
export const publicProcedure = t.procedure;
// Protected endpoint - requires valid JWT
export const protectedProcedure = t.procedure.use(isAuthenticated);
// Admin endpoint - requires admin role
export const adminProcedure = t.procedure.use(isAdmin);Usage Example:
export const usersRouter = router({
// Query - fetch data
list: protectedProcedure
.input(z.object({
page: z.number().default(1),
limit: z.number().default(10),
keyword: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
// ctx.user contains authenticated user info
const client = ctx.services.getDefault();
const data = await client.get('/api/users', { query: input });
return { code: 200, message: 'success', data };
}),
// Mutation - modify data
create: adminProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email(),
role: z.string(),
}))
.mutation(async ({ input, ctx }) => {
const client = ctx.services.getDefault();
const data = await client.post('/api/users', { body: input });
return { code: 201, message: 'Created', data };
}),
});Context โ
Each request creates an independent context:
interface Context {
req: Request; // Express request object
res: Response; // Express response object
user: JWTPayload | null; // Authenticated user (via JWT)
traceId: string; // Distributed tracing ID (UUID)
services: ServiceRegistry; // Backend service registry
}Context Creation Flow:
- Parse JWT Token from
Authorizationheader - Verify token validity and extract user information
- Generate unique
traceId(for distributed tracing) - Inject
ServiceRegistry(backend service collection)
JWT Token Structure โ
interface JWTPayload {
id: string; // User ID
name: string; // Username
email: string; // Email
role: {
id: string; // Role ID
name: string; // Role name (e.g., admin, user)
label: string; // Role display name
permissions: string[]; // Permission list (e.g., ["users:*", "documents:view"])
};
}Token Usage:
// Client sends request
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// Server automatically parses and injects into ctx.user
const userId = ctx.user.id;
const userPermissions = ctx.user.role.permissions;Permission System โ
Supports flexible wildcard permission matching:
| Permission Format | Description | Example |
|---|---|---|
* | All permissions (super admin) | Can perform any operation |
{resource}:* | All operations on module | users:* = all user module permissions |
{resource}:{action} | Specific operation | users:view = view users only |
Permission Check Example:
// Check permission in middleware
export const requirePermission = (permission: string) => {
return t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
const hasPermission = ctx.user.role.permissions.some(p =>
p === '*' ||
p === permission ||
(p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
);
if (!hasPermission) {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return next();
});
};
// Usage
export const deleteUser = protectedProcedure
.use(requirePermission('users:delete'))
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
// Only users with users:delete permission can execute
});Service Registry and Discovery โ
Configure multiple backend services via environment variables:
# Python FastAPI
HALOLIGHT_API_PYTHON_URL=http://api-python:8000
# Bun Hono
HALOLIGHT_API_BUN_URL=http://api-bun:3000
# Java Spring Boot
HALOLIGHT_API_JAVA_URL=http://api-java:8080
# Go Fiber
HALOLIGHT_API_GO_URL=http://api-go:8081Service Priority: By configuration order, the first available service is used as default.
Usage Example:
// Use default service (highest priority)
const client = ctx.services.getDefault();
const data = await client.get('/api/users');
// Use specific service
const pythonClient = ctx.services.get('python');
const stats = await pythonClient.get('/api/dashboard/stats');
// Failover: automatically switch to next service if default is unavailable
try {
const data = await ctx.services.getDefault().get('/api/users');
} catch (error) {
// ServiceRegistry automatically retries other services
}Response Format โ
All APIs follow a unified response structure:
// Standard response
interface APIResponse<T> {
code: number; // HTTP status code (200, 201, 400, 500...)
message: string; // Human-readable message (success, error, ...)
data: T | null; // Response data (on success) or null (on failure)
}
// Paginated response
interface PaginatedResponse<T> {
code: number;
message: string;
data: {
list: T[]; // Data list
total: number; // Total record count
page: number; // Current page number
limit: number; // Items per page
totalPages?: number; // Total pages (optional)
};
}Examples:
// Success response
{
"code": 200,
"message": "success",
"data": {
"id": "1",
"name": "John Doe",
"email": "john@example.com"
}
}
// Paginated response
{
"code": 200,
"message": "success",
"data": {
"list": [{ "id": "1", "name": "User 1" }],
"total": 100,
"page": 1,
"limit": 10,
"totalPages": 10
}
}
// Error response (tRPC auto-formatted)
{
"error": {
"code": "UNAUTHORIZED",
"message": "Not authenticated"
}
}Authentication โ
JWT Dual Token โ
Access Token: 15 minutes validity, used for API requests
Refresh Token: 7 days validity, used to refresh Access TokenRequest Header โ
Authorization: Bearer <access_token>Refresh Flow โ
// Client example
const refreshToken = async () => {
const refreshToken = localStorage.getItem('refreshToken');
const result = await trpc.auth.refresh.mutate({ refreshToken });
localStorage.setItem('accessToken', result.data.accessToken);
localStorage.setItem('refreshToken', result.data.refreshToken);
return result.data.accessToken;
};
// tRPC client configuration - auto refresh
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3002/trpc',
async headers() {
let token = localStorage.getItem('accessToken');
// Auto refresh if token expired
if (isTokenExpired(token)) {
token = await refreshToken();
}
return {
authorization: `Bearer ${token}`,
};
},
}),
],
});Error Handling โ
tRPC Error Types โ
import { TRPCError } from '@trpc/server';
// 400 - Bad request
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Invalid input',
});
// 401 - Unauthorized
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Not authenticated',
});
// 403 - Forbidden
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Insufficient permissions',
});
// 404 - Not found
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Resource not found',
});
// 409 - Conflict
throw new TRPCError({
code: 'CONFLICT',
message: 'Email already exists',
});
// 500 - Internal server error
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Something went wrong',
});Error Response Format โ
{
"error": {
"code": "UNAUTHORIZED",
"message": "Not authenticated",
"data": {
"code": "UNAUTHORIZED",
"httpStatus": 401,
"path": "auth.login"
}
}
}Client Usage โ
React + @tanstack/react-query โ
import { createTRPCReact } from '@trpc/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';
// Create tRPC React hooks
const trpc = createTRPCReact<AppRouter>();
// Create tRPC client
const trpcClient = trpc.createClient({
transformer: superjson, // Support Date, Map, Set, etc.
links: [
httpBatchLink({
url: 'http://localhost:3002/trpc',
headers() {
return {
authorization: `Bearer ${localStorage.getItem('token')}`,
};
},
}),
],
});
// Create React Query client
const queryClient = new QueryClient();
// Root component
function App() {
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
</trpc.Provider>
);
}
// Use tRPC hooks
function UserList() {
// Query - auto-managed loading state, caching, refetching
const { data, isLoading, error } = trpc.users.list.useQuery({
page: 1,
limit: 10,
});
// Mutation - auto-managed loading state, error handling
const createUser = trpc.users.create.useMutation({
onSuccess: () => {
// Auto refresh user list
trpc.users.list.invalidate();
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => createUser.mutate({
name: 'New User',
email: 'new@example.com',
role: 'user',
})}>
Create User
</button>
{data?.data.list.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}Next.js App Router โ
// app/api/trpc/[trpc]/route.ts - tRPC API route
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };
// app/providers.tsx - tRPC Provider
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import superjson from 'superjson';
import type { AppRouter } from '@/server/routers';
const trpc = createTRPCReact<AppRouter>();
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
transformer: superjson,
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}
// app/page.tsx - Server Component
import { createCaller } from '@/server/routers';
export default async function Page() {
const caller = createCaller({ req: {}, res: {}, user: null });
const stats = await caller.dashboard.getStats();
return <div>Total Users: {stats.data.totalUsers}</div>;
}Vue 3 + TanStack Query โ
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';
// Create tRPC client
const trpc = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
httpBatchLink({
url: 'http://localhost:3002/trpc',
headers() {
return {
authorization: `Bearer ${localStorage.getItem('token')}`,
};
},
}),
],
});
// Use in component
export default {
setup() {
const queryClient = useQueryClient();
// Query
const { data, isLoading } = useQuery({
queryKey: ['users', { page: 1 }],
queryFn: () => trpc.users.list.query({ page: 1, limit: 10 }),
});
// Mutation
const createUser = useMutation({
mutationFn: (user) => trpc.users.create.mutate(user),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return { data, isLoading, createUser };
},
};Vanilla TypeScript โ
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from 'halolight-bff';
import superjson from 'superjson';
const client = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
httpBatchLink({
url: 'http://localhost:3002/trpc',
headers() {
return {
authorization: `Bearer ${localStorage.getItem('token')}`,
};
},
}),
],
});
// Usage (full type inference)
const users = await client.users.list.query({ page: 1 });
console.log(users.data.list); // TS automatically infers type
const newUser = await client.users.create.mutate({
name: 'John',
email: 'john@example.com',
role: 'user',
});Development Guide โ
Adding New Router โ
- Create new router file:
// src/routers/products.ts
import { z } from 'zod';
import { router, protectedProcedure, adminProcedure } from '../trpc';
export const productsRouter = router({
// Query - get product list
list: protectedProcedure
.input(z.object({
page: z.number().default(1),
limit: z.number().default(10),
category: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
const client = ctx.services.getDefault();
const data = await client.get('/api/products', { query: input });
return { code: 200, message: 'success', data };
}),
// Query - get product details
byId: protectedProcedure
.input(z.object({
id: z.string(),
}))
.query(async ({ input, ctx }) => {
const client = ctx.services.getDefault();
const data = await client.get(`/api/products/${input.id}`);
return { code: 200, message: 'success', data };
}),
// Mutation - create product (requires admin permission)
create: adminProcedure
.input(z.object({
name: z.string().min(2),
price: z.number().positive(),
category: z.string(),
}))
.mutation(async ({ input, ctx }) => {
const client = ctx.services.getDefault();
const data = await client.post('/api/products', { body: input });
return { code: 201, message: 'Created', data };
}),
// Mutation - update product
update: adminProcedure
.input(z.object({
id: z.string(),
name: z.string().min(2).optional(),
price: z.number().positive().optional(),
}))
.mutation(async ({ input, ctx }) => {
const { id, ...updateData } = input;
const client = ctx.services.getDefault();
const data = await client.put(`/api/products/${id}`, { body: updateData });
return { code: 200, message: 'Updated', data };
}),
// Mutation - delete product
delete: adminProcedure
.input(z.object({
id: z.string(),
}))
.mutation(async ({ input, ctx }) => {
const client = ctx.services.getDefault();
await client.delete(`/api/products/${input.id}`);
return { code: 200, message: 'Deleted', data: null };
}),
});- Register in root router:
// src/routers/index.ts
import { router } from '../trpc';
import { authRouter } from './auth';
import { usersRouter } from './users';
import { productsRouter } from './products'; // Import new router
export const appRouter = router({
auth: authRouter,
users: usersRouter,
products: productsRouter, // Register new router
// ... other routers
});
export type AppRouter = typeof appRouter;- Client usage:
// Type auto-inference, no manual definition needed
const products = await trpc.products.list.query({ page: 1 });
const product = await trpc.products.byId.query({ id: '1' });
const newProduct = await trpc.products.create.mutate({
name: 'iPhone 15',
price: 999,
category: 'electronics',
});Adding Custom Middleware โ
// src/middleware/rateLimit.ts
import { TRPCError } from '@trpc/server';
import { t } from '../trpc';
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
export const rateLimit = (maxRequests: number, windowMs: number) => {
return t.middleware(({ ctx, next }) => {
const key = ctx.user?.id || ctx.req.ip;
const now = Date.now();
const record = rateLimitMap.get(key);
if (!record || now > record.resetAt) {
rateLimitMap.set(key, { count: 1, resetAt: now + windowMs });
return next();
}
if (record.count >= maxRequests) {
throw new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: 'Rate limit exceeded',
});
}
record.count++;
return next();
});
};
// Usage
export const limitedProcedure = protectedProcedure.use(
rateLimit(10, 60000) // Max 10 requests per minute
);Adding Schema Validation โ
// src/schemas/product.ts
import { z } from 'zod';
export const productSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2).max(100),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'books']),
stock: z.number().int().nonnegative(),
createdAt: z.date(),
updatedAt: z.date(),
});
export const createProductSchema = productSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const updateProductSchema = createProductSchema.partial();
// Use in router
export const productsRouter = router({
create: adminProcedure
.input(createProductSchema)
.mutation(async ({ input, ctx }) => {
// input is already validated by Zod, type-safe
}),
update: adminProcedure
.input(z.object({
id: z.string(),
data: updateProductSchema,
}))
.mutation(async ({ input, ctx }) => {
// ...
}),
});Common Commands โ
# Development
pnpm dev # Start dev server (hot reload)
pnpm dev:watch # Start dev server (file watch)
# Build
pnpm build # Build for production
pnpm start # Start production server
# Testing
pnpm test # Run tests
pnpm test:watch # Run tests in watch mode
pnpm test:coverage # Generate test coverage
# Code quality
pnpm lint # Run ESLint
pnpm lint:fix # Auto-fix lint errors
pnpm type-check # TypeScript type checking
pnpm format # Prettier code formattingDeployment โ
Docker โ
# Build image
docker build -t halolight-bff .
# Run container
docker run -p 3002:3002 \
-e JWT_SECRET=your-secret-key \
-e HALOLIGHT_API_PYTHON_URL=http://api-python:8000 \
halolight-bffDocker Compose โ
# docker-compose.yml
version: '3.8'
services:
bff:
build: .
ports:
- "3002:3002"
environment:
- NODE_ENV=production
- JWT_SECRET=${JWT_SECRET}
- HALOLIGHT_API_PYTHON_URL=http://api-python:8000
- HALOLIGHT_API_BUN_URL=http://api-bun:3000
- HALOLIGHT_API_JAVA_URL=http://api-java:8080
depends_on:
- api-python
- api-bun
- api-java
restart: unless-stopped
api-python:
image: halolight-api-python
ports:
- "8000:8000"
api-bun:
image: halolight-api-bun
ports:
- "3000:3000"
api-java:
image: halolight-api-java
ports:
- "8080:8080"docker-compose up -dProduction Configuration โ
NODE_ENV=production
PORT=3002
HOST=0.0.0.0
# Strong secret (at least 32 characters)
JWT_SECRET=your-production-secret-key-with-at-least-32-characters
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d
# Restrict CORS
CORS_ORIGIN=https://your-frontend.com
# Production logging
LOG_LEVEL=warn
# Backend services
HALOLIGHT_API_PYTHON_URL=https://api-python.production.com
HALOLIGHT_API_BUN_URL=https://api-bun.production.com
HALOLIGHT_API_JAVA_URL=https://api-java.production.comPerformance Optimization โ
1. Enable Request Batching โ
tRPC automatically batches multiple concurrent requests to reduce network overhead:
// Client configuration
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3002/trpc',
maxURLLength: 2083, // Max URL length
}),
],
});
// These three requests are automatically batched into one HTTP request
const [users, stats, notifications] = await Promise.all([
trpc.users.list.query({ page: 1 }),
trpc.dashboard.getStats.query(),
trpc.notifications.unreadCount.query(),
]);2. Use DataLoader to Avoid N+1 Queries โ
import DataLoader from 'dataloader';
// Create DataLoader
const userLoader = new DataLoader(async (ids: string[]) => {
const users = await db.user.findMany({
where: { id: { in: ids } },
});
return ids.map(id => users.find(u => u.id === id));
});
// Inject in context
export const createContext = (opts: CreateExpressContextOptions) => {
return {
...opts,
loaders: {
user: userLoader,
},
};
};
// Use in router
export const postsRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
const posts = await db.post.findMany();
// Batch load author info, avoid N+1 queries
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
...post,
author: await ctx.loaders.user.load(post.authorId),
}))
);
return postsWithAuthors;
}),
});3. Caching Strategy โ
// Use Redis caching
import Redis from 'ioredis';
const redis = new Redis();
export const dashboardRouter = router({
getStats: protectedProcedure.query(async ({ ctx }) => {
const cacheKey = `dashboard:stats:${ctx.user.id}`;
// Try to get from cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from backend service
const client = ctx.services.getDefault();
const data = await client.get('/api/dashboard/stats');
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(data));
return data;
}),
});4. Rate Limiting โ
import rateLimit from 'express-rate-limit';
// Configure global rate limiting in Express
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Max 100 requests
message: 'Too many requests from this IP',
});
app.use('/trpc', limiter);Security Best Practices โ
1. Use Strong JWT Secret โ
# Generate strong secret (at least 32 characters)
openssl rand -base64 32
# Configure in .env
JWT_SECRET=your-generated-secret-key-with-at-least-32-characters2. Enable HTTPS โ
// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});
}3. Restrict CORS โ
# Allow only specific origin
CORS_ORIGIN=https://your-frontend.com
# Or multiple origins (comma-separated)
CORS_ORIGIN=https://app1.com,https://app2.com4. Input Validation โ
// Strictly validate all inputs with Zod
export const createUser = protectedProcedure
.input(z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().positive().max(150),
role: z.enum(['admin', 'user']),
}))
.mutation(async ({ input, ctx }) => {
// input is strictly validated
});5. Log Sanitization โ
// Use Pino redact configuration
const logger = pino({
redact: {
paths: [
'req.headers.authorization',
'req.body.password',
'req.body.token',
'res.headers["set-cookie"]',
],
remove: true, // Completely remove sensitive fields
},
});Observability โ
Logging System โ
// Use Pino structured logging
import pino from 'pino';
import pinoHttp from 'pino-http';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
});
// HTTP request logging
app.use(pinoHttp({ logger }));
// Use in router
export const usersRouter = router({
create: adminProcedure.mutation(async ({ input, ctx }) => {
logger.info({ userId: ctx.user.id, input }, 'Creating user');
try {
const data = await createUser(input);
logger.info({ userId: ctx.user.id, data }, 'User created');
return data;
} catch (error) {
logger.error({ userId: ctx.user.id, error }, 'Failed to create user');
throw error;
}
}),
});Health Check โ
// Health check endpoint
app.get('/health', async (req, res) => {
try {
// Check backend service connections
const services = await Promise.all([
fetch(`${process.env.HALOLIGHT_API_PYTHON_URL}/health`),
fetch(`${process.env.HALOLIGHT_API_BUN_URL}/health`),
]);
const allHealthy = services.every(s => s.ok);
res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
services: {
python: services[0].ok,
bun: services[1].ok,
},
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message,
});
}
});Monitoring Metrics โ
// Prometheus metrics
import promClient from 'prom-client';
// Create registry
const register = new promClient.Registry();
// Collect default metrics
promClient.collectDefaultMetrics({ register });
// Custom metrics
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
registers: [register],
});
// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});Troubleshooting โ
Q: Port Already in Use โ
A: Change PORT in .env or terminate the process using the port:
# Find process using port
lsof -i :3002
# Kill process
kill -9 <PID>
# Or change port
echo "PORT=3003" >> .envQ: CORS Errors โ
A: Update CORS_ORIGIN in .env to allow your origin:
# Development - allow all origins
CORS_ORIGIN=*
# Production - specify origin
CORS_ORIGIN=https://your-frontend.comQ: Token Verification Fails โ
A: Ensure JWT_SECRET is consistent across all environments:
# Check JWT_SECRET consistency
echo $JWT_SECRET
# Regenerate token
curl -X POST http://localhost:3002/trpc/auth.login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"password"}'Q: Backend Service Connection Failed โ
A: Check if backend services are running and URLs are configured correctly:
# Check service health
curl http://localhost:8000/health
curl http://localhost:3000/health
# Check environment variables
echo $HALOLIGHT_API_PYTHON_URL
echo $HALOLIGHT_API_BUN_URL
# Test connection
curl http://localhost:3002/healthQ: Type Inference Not Working โ
A: Ensure AppRouter type is correctly exported and imported in client:
// Server - src/routers/index.ts
export const appRouter = router({
// ... routers
});
export type AppRouter = typeof appRouter; // Must export type
// Client - ensure importing from correct path
import type { AppRouter } from 'halolight-bff'; // NPM package
// or
import type { AppRouter } from '@/server/routers'; // MonorepoComparison with Other Gateways โ
| Feature | tRPC BFF | GraphQL | REST API | gRPC |
|---|---|---|---|---|
| Type Safety | โญโญโญโญโญ | โญโญโญโญ | โญโญ | โญโญโญโญ |
| Developer Experience | โญโญโญโญโญ | โญโญโญ | โญโญโญ | โญโญ |
| Performance | โญโญโญโญ | โญโญโญ | โญโญโญโญ | โญโญโญโญโญ |
| Learning Curve | โญโญโญโญ | โญโญ | โญโญโญโญโญ | โญโญ |
| Ecosystem | โญโญโญ | โญโญโญโญโญ | โญโญโญโญโญ | โญโญโญ |
| Documentation | โญโญโญโญ | โญโญโญโญ | โญโญโญโญโญ | โญโญโญ |