Building a Real-Time Football Livescore App with React and Node.js
As a self-taught developer from Nigeria, I wanted to build something that combined my passion for football with my growing skills in web development. The result? A real-time football livescore application that updates match scores, statistics, and events as they happen.
In this article, I'll walk you through the technical decisions, challenges, and solutions I encountered while building this project. Whether you're a beginner or intermediate developer, you'll find practical insights you can apply to your own real-time applications.
The Problem I Wanted to Solve
Football fans want instant updates. Refreshing a page every minute to check scores isn't good enough anymore. I needed to build an app that:
- Displays live match scores as they happen
- Shows multiple matches simultaneously
- Updates without requiring page refreshes
- Works smoothly on mobile devices (because most Nigerian football fans watch on mobile)
- Handles poor internet connectivity gracefully
Tech Stack Decisions
Frontend:
- React (for building interactive UI components)
- CSS3 (for responsive design and animations)
- Axios (for API requests)
Backend:
- Node.js with Express.js (for the server)
- REST API integration (football data provider)
- WebSocket/Server-Sent Events for real-time updates
Why React? Component-based architecture makes it easy to create reusable match cards, scoreboards, and stat displays. The virtual DOM ensures smooth updates when scores change.
Why Node.js? JavaScript on both frontend and backend means I could share code and move faster. Plus, Node's event-driven architecture is perfect for real-time applications.
Architecture Overview
Here's how the system works:
- Frontend (React) requests initial match data from my backend
- Backend (Node.js) fetches data from a third-party football API
- Real-time updates push score changes to connected clients
- State management in React keeps the UI in sync with live data
Key Features I Built
1. Live Score Updates
The core feature. Matches update in real-time without refreshing:
// Simplified real-time update logic
useEffect(() => {
const eventSource = new EventSource('/api/live-updates');
eventSource.onmessage = (event) => {
const updatedMatch = JSON.parse(event.data);
setMatches(prevMatches =>
prevMatches.map(match =>
match.id === updatedMatch.id ? updatedMatch : match
)
);
};
return () => eventSource.close();
}, []);
This approach uses Server-Sent Events (SSE) instead of WebSockets because:
- SSE is simpler to implement for one-way data flow (server → client)
- Automatically reconnects if connection drops
- Works over standard HTTP (no special server configuration needed)
2. Match Filtering and Search
Users can filter by:
- League (Premier League, La Liga, Serie A, etc.)
- Match status (Live, Upcoming, Finished)
- Favorite teams
I used React's useMemo hook to optimize filtering performance:
const filteredMatches = useMemo(() => {
return matches.filter(match => {
const leagueMatch = selectedLeague === 'all' || match.league === selectedLeague;
const statusMatch = selectedStatus === 'all' || match.status === selectedStatus;
const searchMatch = match.homeTeam.toLowerCase().includes(searchTerm.toLowerCase()) ||
match.awayTeam.toLowerCase().includes(searchTerm.toLowerCase());
return leagueMatch && statusMatch && searchMatch;
});
}, [matches, selectedLeague, selectedStatus, searchTerm]);
3. Responsive Match Cards
Each match displays:
- Team names and logos
- Current score
- Match time/status
- Key events (goals, cards, substitutions)
The card design adapts seamlessly from mobile to desktop using CSS Grid and Flexbox:
.match-card {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
padding: 1.5rem;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.match-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
@media (max-width: 768px) {
.match-card {
padding: 1rem;
gap: 0.5rem;
}
}
4. Match Statistics Modal
Clicking on any match opens a detailed view with:
- Possession percentage
- Shots on target
- Corner kicks
- Fouls
- Player lineups
I used React portals to render the modal outside the main DOM hierarchy, preventing z-index issues.
Technical Challenges and Solutions
Challenge 1: API Rate Limiting
Most free football APIs have strict rate limits (e.g., 100 requests/day). Making a request every second for live updates would exhaust the limit in minutes.
Solution: Implemented a caching layer in my backend:
// Cache structure
const matchCache = {
data: null,
timestamp: null,
TTL: 30000 // 30 seconds
};
app.get('/api/matches', async (req, res) => {
const now = Date.now();
// Return cached data if still fresh
if (matchCache.data && (now - matchCache.timestamp) < matchCache.TTL) {
return res.json(matchCache.data);
}
// Fetch fresh data
const matches = await fetchFromFootballAPI();
matchCache.data = matches;
matchCache.timestamp = now;
res.json(matches);
});
This reduced API calls by 95% while still providing near-real-time updates.
Challenge 2: Handling Network Failures
In Nigeria, internet connectivity can be unstable. The app needed to handle disconnections gracefully.
Solution: Implemented automatic retry logic with exponential backoff:
const fetchWithRetry = async (url, retries = 3, delay = 1000) => {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
if (retries === 0) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(url, retries - 1, delay * 2);
}
};
Users see a friendly "Reconnecting..." message instead of a broken app.
Challenge 3: Performance with Many Matches
During busy football weekends, 20+ matches might be live simultaneously. Re-rendering all components on every update caused lag.
Solution: Two optimizations:
- React.memo to prevent unnecessary re-renders:
const MatchCard = React.memo(({ match }) => {
// Component logic
}, (prevProps, nextProps) => {
return prevProps.match.score === nextProps.match.score &&
prevProps.match.status === nextProps.match.status;
});
- Virtualization for long lists using a simple windowing technique (only rendering visible matches).
Challenge 4: Mobile Data Consumption
Real-time updates can consume significant mobile data, which is expensive in Nigeria.
Solution: Added a "Data Saver" mode:
- Reduces update frequency from 30s to 2 minutes
- Compresses images
- Disables auto-playing videos
- Shows a data usage indicator
Backend Structure
My Express.js backend is organized like this:
server/
├── routes/
│ ├── matches.js
│ └── leagues.js
├── services/
│ ├── footballAPI.js
│ └── cache.js
├── middleware/
│ ├── errorHandler.js
│ └── rateLimiter.js
└── server.js
The footballAPI.js service abstracts all third-party API calls, making it easy to switch providers if needed.
Lessons Learned
1. Start Simple, Iterate Later
I initially tried to implement WebSockets, Redis caching, and a complex state management system. I was overwhelmed. I simplified to SSE and in-memory caching, got it working, then improved incrementally.
2. Real-Time Doesn't Always Mean Instant
A 30-second delay for score updates is perfectly acceptable. Users care more about reliability than seeing a goal exactly when it happens.
3. Mobile-First Is Non-Negotiable
Over 80% of users accessed the app on mobile. Designing for mobile first, then scaling up to desktop, was the right call.
4. Error States Matter
Showing "Loading...", "No matches today", or "Connection lost" messages dramatically improved user experience. Don't ignore error states.
Performance Metrics
After optimization:
- Initial load time: 1.8s (on 3G)
- Update latency: 30-45s from real event
- Bundle size: 180KB gzipped
- Lighthouse score: 92/100
What I'd Do Differently
- Use TypeScript from the start - Would have caught many bugs during development
- Implement proper testing - I wrote tests after building everything, which was harder
- Add PWA features earlier - Offline support and install prompts would increase engagement
- Better analytics - Understanding what features users actually use would guide development
Future Improvements
- Push notifications for favorite teams' goals
- Live commentary integration
- Social features (reactions, comments)
- Match predictions using simple ML models
- Video highlights integration (if I can find an affordable API)
Try It Yourself
Building a real-time app is more accessible than you think. Start with:
- A simple Node.js server with Express
- One React component that fetches data
- Add Server-Sent Events for updates
- Deploy on free platforms (Vercel for frontend, Render for backend)
You don't need expensive infrastructure or advanced knowledge. You just need to start and iterate.
Conclusion
This project taught me more than any tutorial could. I dealt with real-world constraints: API limits, poor connectivity, performance issues, and mobile optimization.
As a self-taught developer without a computer science degree, building projects like this is how I prove my skills to potential employers. It's not just about the code—it's about solving real problems that real users face.
If you're learning web development, build something you care about. For me, it was football. For you, it might be something else. The technical skills you gain along the way will transfer to any project.
About the Author: I'm Qayyum Oladimeji, a Full-Stack Developer from Nigeria specializing in React, Next.js, and Node.js. I'm passionate about building applications that work well even in challenging network conditions. Connect with me on LinkedIn or check out my portfolio.
GitHub Repository: Frontend Backend
If you found this helpful, leave a reaction and follow me for more articles about building real-world applications with React, Next.js and Node.js.


Top comments (0)