Skip to main content
Back to Blog
ReactTypeScriptFintechWeb DevelopmentSecurity

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.

6 min read

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:

  1. Start with types: Define your domain models thoroughly
  2. Validate everything: Trust nothing from external sources
  3. Log extensively: But never log sensitive data
  4. Test edge cases: Zero, negative, very large, very small
  5. Plan for failure: Financial systems must degrade gracefully
  6. 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:

For more of my work, visit my portfolio.


Related Articles: