Scaling the Mercury Dashboard - API Best Practices
As our Mercury Dashboard grows, our current API implementation is starting to show its limitations. This post outlines a roadmap for modernizing our approach to data fetching for better maintainability and performance.
After reviewing our current Mercury Dashboard implementation, I've identified several opportunities to improve our data fetching patterns. The dashboard has grown significantly, and it's time to adopt more robust patterns to support this growth.
Current Implementation Overview
Our current approach to API calls has several strengths but also limitations:
- Centralized API Client: We organize API endpoints by module (positionsApi, tournamentsApi, systemApi)
- Environment-Based URLs: Different Mercury environments (A, B, H, R, W, DEV) with localStorage persistence
- Client-Side Data Fetching: All components are client components using useEffect for data fetching
- Component-Level State Management: Each component manages its own state with useState
While this approach worked well initially, we're starting to see code duplication, inconsistent error handling, and potential performance issues as we scale.
Recommended Improvements
1. Move to React Query/SWR
Replace our manual fetch calls with a data fetching library:
// Current approach
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch('...');
const data = await response.json();
setData(data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// React Query approach
const { data, isLoading, error } = useQuery({
queryKey: ['tournaments'],
queryFn: () => tournamentsApi.getTournaments(),
});
Benefits:
- Automatic caching and request deduplication
- Background refetching of stale data
- Built-in loading and error states
- Request cancellation on component unmount
2. Create API Hooks
Convert our API client functions to custom hooks:
// api/hooks/useTournaments.ts
export function useTournaments(options) {
return useQuery({
queryKey: ['tournaments', options],
queryFn: () => tournamentsApi.getTournaments(options),
});
}
// Component usage
const { data, isLoading } = useTournaments({ status: 'active' });
This approach centralizes fetch logic, makes components cleaner, and ensures consistent behavior across the application.
3. Implement Server Components
Our Next.js 15 app could benefit from Server Components for data fetching:
// app/tournaments/page.tsx
export default async function TournamentsPage() {
// Server-side data fetching
const tournaments = await fetchTournaments();
return (
<div>
<TournamentsClient data={tournaments} />
</div>
);
}
// Client component for interactivity
'use client';
function TournamentsClient({ data }) {
// Interactive UI logic here
}
This pattern ensures data is available on initial page load without client-side waterfall requests.
4. Add TypeScript Zod Validation
Runtime validation ensures API responses match expected types:
import { z } from 'zod';
const TournamentSchema = z.object({
id: z.string(),
type: z.string(),
status: z.enum(['pending', 'active', 'completed']),
progress: z.number(),
// Other fields...
});
// Parse and validate API response
const tournaments = TournamentSchema.array().parse(data);
This provides an additional safety layer beyond TypeScript's compile-time checks.
5. Organize by Feature
Restructure our code by feature/domain rather than technical concerns:
src/
features/
tournaments/
api.ts
hooks.ts
types.ts
components/
TournamentCard.tsx
TournamentList.tsx
positions/
api.ts
hooks.ts
types.ts
components/
...
This organization makes it easier to navigate, maintain, and extend features independently.
6. Implement Global UI State
Add lightweight state management with Zustand:
import { create } from 'zustand';
const useAppStore = create((set) => ({
environment: 'R',
setEnvironment: (env) => set({ environment: env }),
// UI preferences
theme: 'dark',
setTheme: (theme) => set({ theme }),
// Filters
filters: {},
setFilter: (key, value) =>
set((state) => ({
filters: { ...state.filters, [key]: value },
})),
}));
This centralizes UI state without the complexity of Redux.
7. Add Error Boundaries
Prevent entire UI crashes with React Error Boundaries:
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
Wrap components that fetch data to provide graceful fallbacks.
8. Improve Caching Strategy
Implement smarter caching with appropriate TTLs:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
},
},
});
This reduces unnecessary refetches while ensuring data freshness.
9. Add Metrics/Telemetry
Track API performance and failures:
// Fetch wrapper with timing
async function fetchWithMetrics(url, options) {
const start = performance.now();
try {
const response = await fetch(url, options);
const end = performance.now();
// Report metrics
reportApiTiming(url, end - start);
return response;
} catch (error) {
// Report error
reportApiError(url, error);
throw error;
}
}
This helps identify performance bottlenecks and API reliability issues.
10. Create API Documentation
Generate documentation from TypeScript interfaces:
/**
* Position entity representing a trading position
* @example
* {
* id: "pos_12345",
* symbol: "BTCUSDT",
* direction: "long",
* status: "open",
* entryPrice: 50000
* }
*/
export interface Position {
/** Unique identifier */
id: string;
/** Trading pair symbol */
symbol: string;
/** Position direction: long or short */
direction: 'long' | 'short';
// Other fields...
}
Use tools like TypeDoc to generate API documentation.
Implementation Plan
To avoid disrupting current development, I recommend phasing these changes:
- Add React Query alongside current fetch implementation (1-2 weeks)
- Create API hooks for new features (ongoing)
- Implement server components for high-traffic pages (2-3 weeks)
- Refactor existing components gradually (4-6 weeks)
Conclusion
Modernizing our data fetching approach will make our codebase more maintainable and performant as the Mercury Dashboard continues to grow. These improvements focus on reducing boilerplate, improving error handling, and enhancing performance - all critical for scaling our platform effectively.
These patterns align with modern React best practices and will make onboarding new developers easier while providing a better experience for our users.
