Building Fintech Applications with React and TypeScript
Learn best practices for building secure, performant fintech applications. Covers real-time data handling, security considerations, and lessons from building InsureSignal and Liquidity Hunters.
Building financial technology applications requires a different mindset than typical web development. After shipping InsureSignal and Liquidity Hunters, I've learned crucial lessons about building production fintech apps.
If you're new to TypeScript, check out my TypeScript best practices guide first. For React patterns, see my React state management guide.
Why Fintech is Different
Financial applications have unique requirements:
- Data accuracy is critical: A rounding error can cost real money
- Security is paramount: You're handling sensitive financial data
- Real-time updates matter: Markets move fast
- Compliance requirements: Regulations vary by jurisdiction
- User trust: One bug can destroy credibility
TypeScript: Your First Line of Defense
TypeScript isn't optional in fintech - it's essential. Strong typing catches errors before they reach production:
// Define precise types for financial data
interface Trade {
id: string;
symbol: string;
side: 'buy' | 'sell';
quantity: number;
price: number;
timestamp: Date;
status: 'pending' | 'filled' | 'cancelled' | 'rejected';
}
// Use branded types for different currency amounts
type USD = number & { readonly brand: unique symbol };
type BTC = number & { readonly brand: unique symbol };
// Prevents accidentally mixing currencies
function convertUsdToBtc(usd: USD, rate: number): BTC {
return (usd / rate) as BTC;
}
Decimal Precision
Never use floating point for money:
// Bad - floating point errors
const total = 0.1 + 0.2; // 0.30000000000000004
// Good - use a decimal library
import Decimal from 'decimal.js';
const total = new Decimal('0.1').plus('0.2'); // 0.3
I use decimal.js in all financial calculations.
Real-Time Data Architecture
Financial apps need efficient real-time updates:
// Custom hook for WebSocket market data
function useMarketData(symbols: string[]) {
const [prices, setPrices] = useState<Map<string, number>>(new Map());
useEffect(() => {
const ws = new WebSocket('wss://market-data.example.com');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
setPrices((prev) => new Map(prev).set(data.symbol, data.price));
};
// Subscribe to symbols
ws.onopen = () => {
ws.send(JSON.stringify({ action: 'subscribe', symbols }));
};
return () => ws.close();
}, [symbols]);
return prices;
}
Throttling Updates
Don't re-render on every tick:
import { throttle } from 'lodash';
const updatePrices = useMemo(
() =>
throttle((newPrices: Map<string, number>) => {
setPrices(newPrices);
}, 100), // Max 10 updates per second
[]
);
Security Best Practices
Input Validation
Never trust user input:
import { z } from 'zod';
const TradeRequestSchema = z.object({
symbol: z.string().regex(/^[A-Z]{1,5}$/),
side: z.enum(['buy', 'sell']),
quantity: z.number().positive().max(1000000),
price: z.number().positive().optional(),
});
// Validate on both client and server
function submitTrade(data: unknown) {
const validated = TradeRequestSchema.parse(data);
// Safe to use validated data
}
API Security
// Rate limiting middleware
const rateLimit = new Map<string, number[]>();
function checkRateLimit(userId: string, limit: number = 100): boolean {
const now = Date.now();
const windowMs = 60000; // 1 minute window
const userRequests = rateLimit.get(userId) || [];
const recentRequests = userRequests.filter((t) => now - t < windowMs);
if (recentRequests.length >= limit) {
return false;
}
rateLimit.set(userId, [...recentRequests, now]);
return true;
}
Sensitive Data Handling
// Never log sensitive data
function logTrade(trade: Trade, user: User) {
console.log({
tradeId: trade.id,
symbol: trade.symbol,
// Don't log: user.ssn, user.accountNumber, etc.
userId: user.id, // Use ID, not PII
});
}
// Mask account numbers in UI
function maskAccountNumber(account: string): string {
return `****${account.slice(-4)}`;
}
State Management for Complex Financial Data
For InsureSignal, I needed to manage complex policy and risk data:
// Zustand store for financial state
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface PortfolioState {
positions: Map<string, Position>;
orders: Order[];
balance: Decimal;
actions: {
updatePosition: (symbol: string, quantity: number) => void;
addOrder: (order: Order) => void;
setBalance: (amount: Decimal) => void;
};
}
const usePortfolioStore = create<PortfolioState>()(
immer((set) => ({
positions: new Map(),
orders: [],
balance: new Decimal(0),
actions: {
updatePosition: (symbol, quantity) =>
set((state) => {
const current = state.positions.get(symbol);
if (current) {
current.quantity = quantity;
}
}),
addOrder: (order) =>
set((state) => {
state.orders.push(order);
}),
setBalance: (amount) =>
set((state) => {
state.balance = amount;
}),
},
}))
);
Data Visualization
Financial data needs clear, performant charts:
import { Line } from 'react-chartjs-2';
import { useMemo } from 'react';
function PriceChart({ data }: { data: PricePoint[] }) {
const chartData = useMemo(
() => ({
labels: data.map((d) => formatTime(d.timestamp)),
datasets: [
{
label: 'Price',
data: data.map((d) => d.price),
borderColor: '#7c3aed',
tension: 0.1,
},
],
}),
[data]
);
return (
<Line
data={chartData}
options={{
responsive: true,
animation: false, // Disable for real-time performance
scales: {
y: {
beginAtZero: false,
},
},
}}
/>
);
}
For large datasets, consider lightweight-charts - it's what TradingView uses.
Error Handling
Financial errors need special treatment:
class FinancialError extends Error {
constructor(
message: string,
public code: string,
public recoverable: boolean,
public userMessage: string
) {
super(message);
}
}
// Specific error types
class InsufficientFundsError extends FinancialError {
constructor(required: Decimal, available: Decimal) {
super(
`Insufficient funds: required ${required}, available ${available}`,
'INSUFFICIENT_FUNDS',
true,
'You don\'t have enough funds for this transaction'
);
}
}
// Error boundary for financial components
function FinancialErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary
fallback={
<div className="error-state">
<p>Unable to display financial data</p>
<button onClick={() => window.location.reload()}>
Refresh
</button>
</div>
}
onError={(error) => {
// Always log financial errors
logToService('financial_error', error);
}}
>
{children}
</ErrorBoundary>
);
}
Testing Financial Logic
Test edge cases rigorously:
describe('Trade Calculations', () => {
it('handles very small quantities', () => {
const trade = calculateTrade({
quantity: new Decimal('0.00000001'),
price: new Decimal('50000'),
});
expect(trade.total.toString()).toBe('0.0005');
});
it('handles very large quantities', () => {
const trade = calculateTrade({
quantity: new Decimal('1000000'),
price: new Decimal('0.0001'),
});
expect(trade.total.toString()).toBe('100');
});
it('rounds to correct precision', () => {
const result = roundToDecimals(
new Decimal('123.456789'),
2
);
expect(result.toString()).toBe('123.46');
});
});
Lessons from Production
Building InsureSignal and Liquidity Hunters taught me:
- Start with types: Define your domain models thoroughly
- Validate everything: Trust nothing from external sources
- Log extensively: But never log sensitive data
- Test edge cases: Zero, negative, very large, very small
- Plan for failure: Financial systems must degrade gracefully
- Monitor in real-time: Set up alerts for anomalies
Conclusion
Building fintech requires extra diligence, but TypeScript and React provide excellent foundations. The key is treating every line of code as potentially handling real money - because it is.
Check out my fintech projects:
- InsureSignal - Insurance lead platform
- Liquidity Hunters - Trading analytics
For more of my work, visit my portfolio.
Related Articles: