The frontend is a React 19 application built with TypeScript and Vite. It uses connect-query (connect-rpc + TanStack Query) for type-safe API communication with the gRPC backend.
Technology Stack
Category Technology Framework React 19 Language TypeScript Build Tool Vite API Client connect-query (connect-rpc + TanStack Query) HTTP Client @connectrpc/connect-web Mocking MSW (Mock Service Worker) Code Quality Biome (lint + format)
Prerequisites
Quick Start
Navigate to frontend directory
Start development server
The dev server starts at:
The development server includes hot module replacement (HMR) for instant updates when you edit code.
Directory Structure
frontend/src/
├── app/
│ ├── App.tsx # Root component
│ ├── main.tsx # Entry point, MSW bootstrap
│ └── providers/
│ └── app-provider.tsx # QueryClient and transport setup
├── features/
│ ├── auth/ # Authentication UI (login/signup)
│ ├── greeter/ # Greeter service demo
│ └── gateway/ # Gateway service demo
├── gen/ # Auto-generated from Protocol Buffers
│ ├── greeter/v1/ # Greeter TypeScript types
│ ├── gateway/v1/ # Gateway TypeScript types
│ └── ... # Other service types
├── interceptors/
│ └── auth-interceptor.ts # JWT token injection
├── lib/
│ ├── transport.ts # connect-web transport config
│ └── query-client.ts # TanStack Query client config
├── testing/
│ ├── browser.ts # MSW worker setup
│ ├── handlers.ts # MSW request handlers
│ └── test-utils.tsx # Testing utilities
└── types/
└── env.d.ts # TypeScript environment types
Key Directories
app/
Application root, providers, and entry point.
main.tsx: Entry point that conditionally starts MSW based on VITE_USE_MOCK
App.tsx: Main component that renders feature demos
providers/: Context providers for React Query and connect-rpc transport
features/
Feature-based modules following the Bulletproof React pattern.
Each feature contains:
components/ — React components
hooks/ — Custom React hooks (e.g., useGreet for RPC calls)
types/ — TypeScript types
gen/
DO NOT EDIT FILES IN THIS DIRECTORY. This directory is auto-generated by buf generate from Protocol Buffer definitions. All changes will be overwritten.
Generated files include:
TypeScript types for protobuf messages
connect-query hooks for RPC methods
Service definitions
interceptors/
connect-rpc interceptors for cross-cutting concerns.
auth-interceptor.ts: Adds JWT token from localStorage to Authorization header
lib/
Shared utilities and configuration.
transport.ts: Configures connect-web transport with base URL and interceptors
query-client.ts: TanStack Query client with retry and cache settings
testing/
Mock Service Worker (MSW) setup for development without a backend.
browser.ts: MSW worker instance
handlers.ts: Mock request handlers for greeter and gateway APIs
test-utils.tsx: Testing utilities (for future unit tests)
Environment Configuration
Frontend configuration is managed through .env files and Vite’s environment variables.
frontend/.env
# API Gateway URL (Traefik)
VITE_API_BASE_URL = http://localhost:30081
# Enable MSW mock mode
VITE_USE_MOCK = false
Environment Variables
Variable Default Description VITE_API_BASE_URLhttp://localhost:30081Backend API base URL VITE_USE_MOCKfalseEnable MSW for mock API responses
All environment variables prefixed with VITE_ are exposed to the browser at build time. Never include secrets in these variables.
Development Modes
Real Backend
Mock Mode (MSW)
Connect to Real Services Use this mode when developing against actual backend services.
Start backend services
Docker Compose
Kubernetes + Tilt
Configure environment
Ensure frontend/.env points to Traefik: VITE_API_BASE_URL = http://localhost:30081
VITE_USE_MOCK = false
Access application
Open http://localhost:5173 The frontend will make real API calls to:
http://localhost:30081/greeter.v1.GreeterService/Greet
http://localhost:30081/gateway.v1.GatewayService/InvokeCustom
http://localhost:30081/auth/login
etc.
This is the recommended mode for most development since it tests real integrations.
Mock Backend with MSW Use this mode when you want to develop frontend features without running the backend.
Enable mock mode
Edit frontend/.env: VITE_API_BASE_URL = http://localhost:30081
VITE_USE_MOCK = true # Enable mocks
Start dev server
You’ll see MSW start in the browser console: How MSW Works When VITE_USE_MOCK=true, the entry point (app/main.tsx) starts the MSW service worker: async function bootstrap () {
if ( import . meta . env . VITE_USE_MOCK === 'true' ) {
const { worker } = await import ( '../testing/browser' );
await worker . start ({ onUnhandledRequest: 'bypass' });
}
// ... render app
}
Mock Handlers MSW handlers are defined in src/testing/handlers.ts: export const handlers = [
http . post ( ` ${ baseUrl } /greeter.v1.GreeterService/Greet` , async ({ request }) => {
const body = await request . json () as { name ?: string };
const name = body . name || 'World' ;
return HttpResponse . json ({
message: `Hello ${ name } from mocked greeter!` ,
externalStatus: 200 ,
externalBodyLength: 321 ,
});
}),
// ... more handlers
];
Adding New Mocks To mock a new endpoint:
Add a handler to src/testing/handlers.ts:
http . post ( ` ${ baseUrl } /myservice.v1.MyService/MyMethod` , async ({ request }) => {
// Parse request
const body = await request . json ();
// Return mock response
return HttpResponse . json ({ result: 'mocked' });
})
Restart dev server (or HMR will reload automatically)
MSW intercepts requests in the browser, so network tools like Chrome DevTools will still show the requests—they’re just handled by the service worker instead of the network.
API Communication
connect-query Integration
The frontend uses connect-query to call backend gRPC services over HTTP.
Transport Setup
Transport is configured in src/lib/transport.ts:
import { createConnectTransport } from '@connectrpc/connect-web' ;
import { authInterceptor } from '../interceptors/auth-interceptor' ;
export const transport = createConnectTransport ({
baseUrl: import . meta . env . VITE_API_BASE_URL || 'http://localhost:30081' ,
interceptors: [ authInterceptor ],
});
Query Client Setup
TanStack Query client is configured in src/lib/query-client.ts:
import { QueryClient } from '@tanstack/react-query' ;
export const queryClient = new QueryClient ({
defaultOptions: {
queries: {
retry: 1 ,
refetchOnWindowFocus: false ,
},
},
});
Making RPC Calls
Generated hooks make RPC calls simple:
import { useQuery } from '@connectrpc/connect-query' ;
import { greet } from '../gen/greeter/v1/greeter-GreeterService_connectquery' ;
function GreeterDemo () {
const { data , isLoading } = useQuery ( greet , { name: 'World' });
if ( isLoading ) return < p > Loading ...</ p > ;
return < p >{data?. message } </ p > ;
}
Auth Interceptor
The auth interceptor (src/interceptors/auth-interceptor.ts) automatically adds JWT tokens to requests:
import type { Interceptor } from '@connectrpc/connect' ;
export const authInterceptor : Interceptor = ( next ) => async ( req ) => {
const token = localStorage . getItem ( 'token' );
if ( token ) {
req . header . set ( 'Authorization' , `Bearer ${ token } ` );
}
return next ( req );
};
This ensures all RPC calls include the user’s authentication token if available.
Available Scripts
Development
Starts Vite dev server at http://localhost:5173 with:
Hot module replacement (HMR)
Fast refresh for React components
Source maps for debugging
Build
Runs TypeScript type checking (tsc -b) followed by production build (vite build).
Output is written to dist/:
frontend/dist/
├── index.html
├── assets/
│ ├── index-[hash].js
│ └── index-[hash].css
└── ...
Preview
Serves the production build locally for testing:
Code Quality
# Check for lint errors
npm run lint
# Auto-fix lint errors
npm run lint:fix
Uses Biome for fast linting. # Format code
npm run format
# Check formatting (without writing)
npm run format:check
Uses Biome for consistent code formatting.
The gen/ directory is excluded from Biome checks since it’s auto-generated.
Code Generation
When backend proto files change, regenerate TypeScript types:
Update proto definitions
Edit files in proto/ (in the project root).
Run buf generate
From project root: This updates:
frontend/src/gen/ — TypeScript types and connect-query hooks
services/gen/go/ — Go protobuf code
Restart dev server
Vite should auto-reload, but you may need to restart: # Stop dev server (Ctrl+C), then:
npm run dev
Never manually edit files in src/gen/. All changes will be overwritten by buf generate.
Project Configuration
Vite Config
vite.config.ts:
import { defineConfig } from 'vite' ;
import react from '@vitejs/plugin-react' ;
export default defineConfig ({
plugins: [ react ()] ,
server: {
host: '0.0.0.0' , // Allow external connections
port: 5173
}
}) ;
TypeScript Config
tsconfig.json and tsconfig.app.json configure TypeScript for React 19 and strict mode.
Key settings:
strict: true — Strict type checking
jsx: "react-jsx" — React 19 JSX transform
moduleResolution: "bundler" — Modern module resolution
Biome Config
biome.json configures linting and formatting:
{
"files" : {
"ignore" : [ "src/gen/**" ] # Exclude generated code
},
"formatter" : {
"indentStyle" : "space" ,
"indentWidth" : 2
},
"linter" : {
"enabled" : true ,
"rules" : {
"recommended" : true
}
}
}
Common Tasks
Add a new feature
Create feature directory
mkdir -p src/features/my-feature/components
mkdir -p src/features/my-feature/hooks
Add components
Create React components in components/: // src/features/my-feature/components/MyComponent.tsx
export function MyComponent () {
return < div > My Feature </ div > ;
}
Add hooks for API calls
// src/features/my-feature/hooks/useMyFeature.ts
import { useQuery } from '@connectrpc/connect-query' ;
import { myMethod } from '../../gen/myservice/v1/myservice-MyService_connectquery' ;
export function useMyFeature ( input : { name : string }) {
return useQuery ( myMethod , input );
}
Import in App.tsx
import { MyComponent } from '../features/my-feature/components/MyComponent' ;
export function App () {
return (
< main >
< MyComponent />
</ main >
);
}
Debug API calls
Troubleshooting
Dev server won’t start
Check port availability:
Kill the process using the port or change the port in vite.config.ts.
API calls fail
Check backend is running:
curl http://localhost:30081/healthz
Check VITE_API_BASE_URL:
Ensure it matches your backend setup (default: http://localhost:30081).
Type errors after proto changes
Regenerate code:
Restart TypeScript server (in VS Code: Cmd+Shift+P → “Restart TS Server”).
MSW not working
Verify environment variable:
cat frontend/.env | grep VITE_USE_MOCK
Should be VITE_USE_MOCK=true.
Check browser console for MSW startup message:
If missing, restart dev server.
Build fails
Run type checking:
Fix any TypeScript errors reported by tsc.
Next Steps
Docker Compose Run backend services locally
Kubernetes + Tilt Advanced local development