Skip to content

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 โ€‹

TechnologyVersionDescription
TypeScript5.9Programming Language
tRPC11RPC Framework
Zod-Data Validation
Express5Web 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 โ€‹

bash
# Clone repository
git clone https://github.com/halolight/halolight-bff.git
cd halolight-bff

# Install dependencies
pnpm install

Environment Variables โ€‹

bash
cp .env.example .env
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:8081

Database Initialization โ€‹

No database required (API gateway does not directly access database).

Start Service โ€‹

bash
# Development mode (hot reload)
pnpm dev

# Production mode
pnpm build
pnpm start

Visit 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 configuration

API Modules โ€‹

HaloLight BFF provides 12 core business modules covering 100+ tRPC endpoints:

ModuleEndpointsDescription
auth8Login, register, token refresh, logout, password management
users8User CRUD, role/status management, profile
dashboard9Statistics, visit trends, sales data, tasks, calendar
permissions7Permission CRUD, tree structure, module permissions, batch operations
roles8Role CRUD, permission assignment, user association
teams9Team CRUD, member management, invitations, permissions
folders8Folder CRUD, tree structure, move, breadcrumb
files9File CRUD, upload, download, move, copy, share
documents10Document CRUD, version control, collaboration, sharing
calendar10Event CRUD, attendee management, RSVP, reminders
notifications7Notification list, unread count, mark read, batch delete
messages9Conversation management, message CRUD, send, read status

Authentication Endpoints โ€‹

ProcedureTypeDescriptionPermission
auth.loginmutationUser loginPublic
auth.registermutationUser registrationPublic
auth.refreshmutationRefresh tokenPublic
auth.logoutmutationLogoutAuthenticated
auth.forgotPasswordmutationForgot passwordPublic
auth.resetPasswordmutationReset passwordPublic
auth.verifyEmailmutationVerify emailPublic
auth.changePasswordmutationChange passwordAuthenticated

User Management Endpoints โ€‹

ProcedureTypeDescriptionPermission
users.listqueryGet user listusers:view
users.byIdqueryGet user detailsusers:view
users.mequeryGet current userAuthenticated
users.createmutationCreate userusers:create
users.updatemutationUpdate userusers:update
users.deletemutationDelete userusers:delete
users.updateRolemutationUpdate user roleusers:update
users.updateStatusmutationUpdate user statususers:update

Complete Endpoint List โ€‹

Dashboard - 9 Endpoints โ€‹

ProcedureTypeDescription
dashboard.getStatsqueryStatistics (users, documents, files, tasks)
dashboard.getVisitsqueryVisit trends (7/30 days)
dashboard.getSalesquerySales data (line chart)
dashboard.getPieDataqueryPie chart data (category distribution)
dashboard.getTasksqueryTodo task list
dashboard.getCalendarqueryToday's calendar
dashboard.getActivitiesqueryRecent activities
dashboard.getNotificationsqueryLatest notifications
dashboard.getProgressqueryProject progress

Permissions - 7 Endpoints โ€‹

ProcedureTypeDescription
permissions.listqueryGet permission list
permissions.treequeryGet permission tree
permissions.byIdqueryGet permission details
permissions.createmutationCreate permission
permissions.updatemutationUpdate permission
permissions.deletemutationDelete permission
permissions.modulesqueryGet permission modules

Roles - 8 Endpoints โ€‹

ProcedureTypeDescription
roles.listqueryGet role list
roles.byIdqueryGet role details
roles.createmutationCreate role
roles.updatemutationUpdate role
roles.deletemutationDelete role
roles.assignPermissionsmutationAssign permissions
roles.removePermissionsmutationRemove permissions
roles.usersqueryGet users in role

Teams - 9 Endpoints โ€‹

ProcedureTypeDescription
teams.listqueryGet team list
teams.byIdqueryGet team details
teams.createmutationCreate team
teams.updatemutationUpdate team
teams.deletemutationDelete team
teams.addMembermutationAdd member
teams.removeMembermutationRemove member
teams.updateMemberRolemutationUpdate member role
teams.membersqueryGet team members

Folders - 8 Endpoints โ€‹

ProcedureTypeDescription
folders.listqueryGet folder list
folders.treequeryGet folder tree
folders.byIdqueryGet folder details
folders.createmutationCreate folder
folders.updatemutationUpdate folder
folders.deletemutationDelete folder
folders.movemutationMove folder
folders.breadcrumbqueryGet breadcrumb path

Files - 9 Endpoints โ€‹

ProcedureTypeDescription
files.listqueryGet file list
files.byIdqueryGet file details
files.uploadmutationUpload file
files.updatemutationUpdate file info
files.deletemutationDelete file
files.movemutationMove file
files.copymutationCopy file
files.downloadqueryGet download link
files.sharemutationShare file

Documents - 10 Endpoints โ€‹

ProcedureTypeDescription
documents.listqueryGet document list
documents.byIdqueryGet document details
documents.createmutationCreate document
documents.updatemutationUpdate document
documents.deletemutationDelete document
documents.versionsqueryGet version history
documents.restoremutationRestore version
documents.sharemutationShare document
documents.unsharemutationUnshare document
documents.collaboratorsqueryGet collaborators

Calendar - 10 Endpoints โ€‹

ProcedureTypeDescription
calendar.eventsqueryGet event list
calendar.byIdqueryGet event details
calendar.createmutationCreate event
calendar.updatemutationUpdate event
calendar.deletemutationDelete event
calendar.addAttendeemutationAdd attendee
calendar.removeAttendeemutationRemove attendee
calendar.rsvpmutationRSVP response
calendar.setRemindermutationSet reminder
calendar.byMonthqueryGet events by month

Notifications - 7 Endpoints โ€‹

ProcedureTypeDescription
notifications.listqueryGet notification list
notifications.unreadCountqueryGet unread count
notifications.markReadmutationMark as read
notifications.markAllReadmutationMark all as read
notifications.deletemutationDelete notification
notifications.deleteAllmutationDelete all
notifications.preferencesqueryGet notification preferences

Messages - 9 Endpoints โ€‹

ProcedureTypeDescription
messages.conversationsqueryGet conversation list
messages.byConversationqueryGet conversation messages
messages.sendmutationSend message
messages.markReadmutationMark as read
messages.deletemutationDelete message
messages.createConversationmutationCreate conversation
messages.deleteConversationmutationDelete conversation
messages.searchquerySearch messages
messages.unreadCountqueryGet unread count

Core Concepts โ€‹

tRPC Procedures โ€‹

tRPC provides three procedure types:

typescript
// 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:

typescript
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:

typescript
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:

  1. Parse JWT Token from Authorization header
  2. Verify token validity and extract user information
  3. Generate unique traceId (for distributed tracing)
  4. Inject ServiceRegistry (backend service collection)

JWT Token Structure โ€‹

typescript
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:

typescript
// 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 FormatDescriptionExample
*All permissions (super admin)Can perform any operation
{resource}:*All operations on moduleusers:* = all user module permissions
{resource}:{action}Specific operationusers:view = view users only

Permission Check Example:

typescript
// 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:

bash
# 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:8081

Service Priority: By configuration order, the first available service is used as default.

Usage Example:

typescript
// 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:

typescript
// 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:

typescript
// 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 Token

Request Header โ€‹

http
Authorization: Bearer <access_token>

Refresh Flow โ€‹

typescript
// 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 โ€‹

typescript
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 โ€‹

json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Not authenticated",
    "data": {
      "code": "UNAUTHORIZED",
      "httpStatus": 401,
      "path": "auth.login"
    }
  }
}

Client Usage โ€‹

React + @tanstack/react-query โ€‹

typescript
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 โ€‹

typescript
// 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 โ€‹

typescript
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 โ€‹

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 โ€‹

  1. Create new router file:
typescript
// 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 };
    }),
});
  1. Register in root router:
typescript
// 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;
  1. Client usage:
typescript
// 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 โ€‹

typescript
// 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 โ€‹

typescript
// 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 โ€‹

bash
# 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 formatting

Deployment โ€‹

Docker โ€‹

bash
# 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-bff

Docker Compose โ€‹

yaml
# 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"
bash
docker-compose up -d

Production Configuration โ€‹

env
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.com

Performance Optimization โ€‹

1. Enable Request Batching โ€‹

tRPC automatically batches multiple concurrent requests to reduce network overhead:

typescript
// 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 โ€‹

typescript
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 โ€‹

typescript
// 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 โ€‹

typescript
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 โ€‹

bash
# Generate strong secret (at least 32 characters)
openssl rand -base64 32

# Configure in .env
JWT_SECRET=your-generated-secret-key-with-at-least-32-characters

2. Enable HTTPS โ€‹

typescript
// 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 โ€‹

bash
# Allow only specific origin
CORS_ORIGIN=https://your-frontend.com

# Or multiple origins (comma-separated)
CORS_ORIGIN=https://app1.com,https://app2.com

4. Input Validation โ€‹

typescript
// 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 โ€‹

typescript
// 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 โ€‹

typescript
// 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 โ€‹

typescript
// 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 โ€‹

typescript
// 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:

bash
# Find process using port
lsof -i :3002

# Kill process
kill -9 <PID>

# Or change port
echo "PORT=3003" >> .env

Q: CORS Errors โ€‹

A: Update CORS_ORIGIN in .env to allow your origin:

bash
# Development - allow all origins
CORS_ORIGIN=*

# Production - specify origin
CORS_ORIGIN=https://your-frontend.com

Q: Token Verification Fails โ€‹

A: Ensure JWT_SECRET is consistent across all environments:

bash
# 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:

bash
# 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/health

Q: Type Inference Not Working โ€‹

A: Ensure AppRouter type is correctly exported and imported in client:

typescript
// 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'; // Monorepo

Comparison with Other Gateways โ€‹

FeaturetRPC BFFGraphQLREST APIgRPC
Type Safetyโญโญโญโญโญโญโญโญโญโญโญโญโญโญโญ
Developer Experienceโญโญโญโญโญโญโญโญโญโญโญโญโญ
Performanceโญโญโญโญโญโญโญโญโญโญโญโญโญโญโญโญ
Learning Curveโญโญโญโญโญโญโญโญโญโญโญโญโญ
Ecosystemโญโญโญโญโญโญโญโญโญโญโญโญโญโญโญโญ
Documentationโญโญโญโญโญโญโญโญโญโญโญโญโญโญโญโญ