Technical Details
Table of contents
- Architecture Overview
- Data Providers
- Provider System
- Image Generation
- Color Extraction
- Team Resolution & Fallback System
- Caching Strategy
- Team Matching System
- Route Loading System
- Data Sources
- Rate Limiting
- Error Handling
- Performance Optimizations
- Logging
- CORS Configuration
- Security Considerations
- Future Enhancements
Architecture Overview
Game Thumbs is a Node.js Express application that dynamically generates sports matchup thumbnails and logos using team and athlete data from multiple providers (ESPN, TheSportsDB, HockeyTech, MLBStats).
Key Components
- Express Server: HTTP server handling API requests
- Provider System: Modular data provider architecture with multiple providers
- Team Matching: Intelligent fuzzy matching with weighted scoring
- Image Generation: Canvas-based rendering with multiple visual styles
- Caching System: Multi-layer caching for performance optimization
- Color Extraction: Automatic dominant color detection from logos
Data Providers
Game Thumbs uses a modular provider architecture to fetch team and athlete data from multiple sources.
ESPN Provider
Leagues: NBA, NFL, MLB, NHL, NCAA, Soccer, and more
Type: Team-based sports
Features:
- Fetches team rosters, logos, and colors from ESPN’s public APIs
- Supports 30+ professional and NCAA leagues
- 24-hour team data caching
- Automatic logo and color extraction
ESPN Athlete Provider
Leagues: UFC, PFL, Bellator (MMA), Tennis (ATP/WTA)
Type: Athlete-based sports
Features:
- Treats individual fighters/athletes as “teams” for matchup generation
- Fetches complete athlete rosters from ESPN Sports Core API v2
- 72-hour athlete data caching with automatic background refresh
- Persistent file-based caching in
.cache/providers/for data retention across server restarts - Smart athlete matching by first name, last name, or full name
- Sport-specific color palettes:
- Tennis: Grass greens, clay browns/oranges, hard court blues, tennis ball yellow-greens
- MMA: Dark blue palette
- Headshot images used as athlete “logos”
- Country flags used as fallback when headshots are unavailable
- Doubles/Team Support: Use
+separator to create composite athlete teams (e.g.,djokovic+federer) - Supports 1,800+ UFC fighters, 500+ PFL fighters, 1,000+ Bellator fighters, 33,800+ tennis players (ATP & WTA)
- Exponential backoff retry with 15s initial delay for rate limit handling
- Conservative request throttling (25 concurrent requests per batch, 200ms delays)
Tennis Cache Time: Initial cache population for tennis (33,800+ athletes) takes 5-30 minutes due to ESPN API rate limiting. Cache persists to disk, so subsequent restarts are instant. The service is fully functional during cache population - it will search available data and complete the cache in the background.
Cache Auto-Refresh: The ESPN Athlete provider automatically refreshes athlete rosters at 95% of cache duration (68.4 hours) to ensure data stays fresh without requiring manual intervention or server restarts.
Doubles/Team Composite Generation: When multiple athletes are specified with +, the provider:
- Resolves each athlete individually
- Creates a composite image with athletes side-by-side (5px spacing)
- Merges names with
/separator (e.g., “Djokovic / Federer”) - Returns composite as a single “team” object for thumbnail generation
TheSportsDB Provider
Leagues: CFL, AHL, OHL, WHL, QMJHL, and international soccer
Type: Team-based sports
Features:
- Community-maintained sports database
- Good coverage for non-US leagues
- Team colors, logos, and basic information
- 24-hour team data caching
HockeyTech Provider
Leagues: PWHL, CHL, OHL, WHL Type: Team-based sports (hockey) Features:
- Official provider for Canadian hockey leagues
- Real-time roster data
- High-quality team information
- 24-hour team data caching
MLBStats Provider
Leagues: MiLB (Triple-A, Double-A, High-A, Single-A), Winter Leagues, Independent Leagues, KBO Type: Team-based sports (baseball) API: statsapi.mlb.com/api/v1 Features:
- Free MLB StatsAPI — no API key required
- Covers all MiLB levels and international baseball leagues tracked by MLB
- SVG logos rasterized to PNG via
svgUtils - Automatic color extraction from team logos
- Season fallback: tries current year, then previous two years
- 24-hour team data caching, 7-day logo caching
- Configurable via
sportIdin league config
FlagCDN Provider
Leagues: Country, Olympics
Type: International country-based matchups
Features:
- High-resolution flag images (2560px) from flagcdn.com
- ISO 3166 alpha-2 and alpha-3 code support (USA, CAN, GBR, etc.)
- Olympic/sports team codes (ROC, OAR, AOR, RPC)
- UK home nations support (ENG, SCT, WAL, NIR)
- Custom color extraction without filtering white colors
- Desaturation (40%) and darkening (30%) for better thumbnail backgrounds
- White color replacement (uses non-white color for both if either is white)
- 7-day caching for country data and extracted colors
- Smart country matching with weighted scoring
Country Resolution: Matches country names, aliases, and ISO codes using intelligent scoring:
- ISO 3-letter codes: 1.0 weight (highest priority)
- Exact name matches: 0.9 weight
- Partial name matches: 0.5-0.8 weight based on similarity
Provider System
Automatic Provider Discovery
Game Thumbs automatically discovers and registers all providers from the providers/ directory at startup. No manual registration required.
How it works:
- Scans
providers/directory for*Provider.jsfiles - Excludes
BaseProvider.js(abstract base class) - Automatically instantiates and registers each provider
- Maps supported leagues to providers
Adding New Providers:
- Create a new file in
providers/following the naming convention:YourNameProvider.js - Extend
BaseProviderand implement required methods:getProviderId(): Return unique provider identifierresolveTeam(): Implement team/athlete resolution logicgetLeagueLogoUrl(): Implement league logo fetching
- Provider is automatically loaded on server restart
Provider Inference: The system automatically infers which provider to use based on the configuration object keys:
{ espn: {...} }→ ESPN Provider{ theSportsDB: {...} }→ TheSportsDB Provider{ hockeytech: {...} }→ HockeyTech Provider{ mlbStats: {...} }→ MLBStats Provider{ espnAthlete: {...} }→ ESPN Athlete Provider{ flagcdn: {...} }→ FlagCDN Provider
No hardcoded provider lists to maintain!
Image Generation
Canvas Rendering
Images are generated using the Node.js canvas library, which provides a Cairo-backed implementation of the HTML5 Canvas API.
Features:
- Server-side image rendering
- Support for multiple image formats (PNG, JPEG)
- Text rendering with custom fonts
- Image compositing and transformations
- Gradient fills and patterns
Visual Styles
Thumbnail & Cover Styles
- Style 1: Diagonal/horizontal split with team colors
- Style 2: Gradient blend between team colors
- Style 3: Minimalist badge with team circles (light background)
- Style 4: Minimalist badge with team circles (dark background)
- Style 5: Grid background with grey diagonal lines and fade to black
- Style 6: Grid background with team color gradient lines and fade to black
- Style 98: 3D embossed design with textures, reflections, metallic VS badge, and league logo
- Style 99: 3D embossed design with textures, reflections, and metallic VS badge (no league logo)
Logo Styles
- Style 1: Diagonal split with dividing line
- Style 2: Side by side arrangement
- Style 3: Circle badges with team colors (league logo overlay)
- Style 4: Square badges with team colors (league logo overlay)
- Style 5: Circle badges with league logo on left (white background)
- Style 6: Square badges with league logo on left (white background)
Logo Selection
The system automatically selects the best logo variant for each context:
- Dark backgrounds: Uses light/default logos
- Light backgrounds: Uses dark variant logos when available
- Team color backgrounds: Selects variant with best contrast
Aspect Ratio Preservation
All league and team logos maintain their aspect ratio:
- Logos are scaled to fit within designated areas
- Transparent backgrounds are preserved
- No distortion or stretching occurs
Color Extraction
When ESPN doesn’t provide team colors, the system automatically extracts them from team logos.
Process
- Download Logo: Fetches the team logo image
- Analyze Pixels: Processes pixel data to find dominant colors
- Filter Colors: Removes neutral/grayscale colors
- Ensure Distinctness: Verifies selected colors are visually distinct
- Cache Results: Stores extracted colors for 24 hours
Algorithm
- Uses k-means clustering to identify dominant colors
- Filters out colors with low saturation (grayscale)
- Ensures minimum color distance between primary and alternate colors
- Prioritizes vibrant, saturated colors
Fallback
If color extraction fails or no vibrant colors are found:
- Primary Color:
#000000(black) - Alternate Color:
#ffffff(white)
Team Resolution & Fallback System
Game Thumbs implements a sophisticated multi-layer fallback system to ensure requests succeed even when teams cannot be found in the primary data source.
Resolution Chain
When resolving a team, the system tries multiple approaches for optimal performance:
- Custom Teams: Checks if team is marked as
custom: trueinteams.json(bypasses all provider lookups) - Primary Provider: Attempts to resolve team from the configured provider(s)
- Alternate Providers (parallel): If team found but has no logo, tries all other providers simultaneously
- Feeder Leagues (parallel): Searches all configured feeder leagues at once
- Fallback League: Falls back to a designated league (e.g., NCAA sports → Men’s Basketball)
- Greyscale League Logo: If
fallback=trueparameter is set, uses greyscale league logo as placeholder - Ultimate Text Fallback: If league logo fails, generates minimal single-letter placeholder on transparent background
Custom Teams (No Provider Lookup)
Teams can be defined as “custom teams” in teams.json with the custom: true flag. These teams bypass all provider lookups entirely:
{
"nfl": {
"nfc": {
"custom": true,
"override": {
"name": "National Football Conference",
"abbreviation": "NFC",
"logoUrl": "https://a.espncdn.com/i/teamlogos/nfl/500/scoreboard/nfc.png",
"color": "#013369",
"alternateColor": "#D50A0A"
}
}
}
}
Required Fields:
name- Team display nameabbreviation- Team abbreviationlogoUrl- Team logo URL
Optional Fields (auto-extracted from logo):
color- Primary color (extracted from logo if omitted)alternateColor- Secondary color (extracted from logo if omitted)
Use Cases:
- Conference/Division teams (NFC vs AFC)
- All-Star teams (Pro Bowl rosters)
- Special event teams
- Historical teams not in current data
- Fantasy/custom league teams
Resolution Priority: Custom teams are checked first before any provider queries, ensuring instant resolution with zero external API calls.
Unsupported League Fallback (Provider Interface)
When a league is not configured in leagues.json, providers can optionally support it through the unconfigured league interface:
- Provider Implementation: Providers implement
canHandleUnconfiguredLeague()andgetUnconfiguredLeagueConfig()methods - Automatic Lookup: When
findLeague()doesn’t find a configured league, it queriesProviderManager.findUnconfiguredLeague() - Provider Check: ProviderManager iterates through all registered providers to find one that can handle the league
- Temporary League Object: The supporting provider creates a temporary league configuration on-the-fly
- Normal Processing: The request continues through the normal flow using that provider
Example (ESPN Provider):
- Request:
GET /eng.w.1/team1/team2/thumb findLeague('eng.w.1')checks configured leagues → not found- Calls
providerManager.findUnconfiguredLeague('eng.w.1') - ProviderManager queries each provider’s
canHandleUnconfiguredLeague()method - ESPN provider’s cache finds
eng.w.1exists under sportsoccer - Returns temporary league object with ESPN provider configuration
- ESPN provider resolves teams and generates thumbnail normally
- Result: Successfully generated matchup thumbnail
This architecture allows any provider to support unconfigured leagues, not just ESPN. The fallback is completely automatic—no special query parameters needed.
ESPN Implementation: The ESPN provider caches all available sports/leagues from ESPN’s Core API on startup (200-400+ leagues across 17+ sports). The cache is self-contained within
ESPNProvider.js, similar to howESPNAthleteProvider.jsmanages its athlete cache. Initial discovery takes 1-2 minutes but runs asynchronously without blocking the server.
Parallel Optimization
Team Pair Resolution: Both teams in a matchup are resolved simultaneously, cutting resolution time in half.
Provider Checks: All providers for a league are queried in parallel using Promise.all().
Feeder League Checks: All feeder leagues are searched simultaneously rather than sequentially.
Shared League Logo Processing: When both teams in a matchup fail, the league logo is downloaded and processed only once, then reused for both teams.
NCAA Fallback Configuration
All NCAA sports use NCAA Men’s Basketball (ncaam) as their fallback league, as it has the most comprehensive roster. Additionally, a generic ncaa league is available that also falls back to ncaam:
{
"ncaa": {
"name": "NCAA",
"logoUrl": "./assets/NCAA.png",
"fallbackLeague": "ncaam"
},
"ncaavb": {
"name": "NCAA Men's Volleyball",
"fallbackLeague": "ncaam"
}
}
This ensures that even obscure NCAA teams can be found if they participate in basketball. The generic ncaa league allows teams to use /ncaa/team1/team2/thumb without specifying a particular sport.
Feeder Leagues
Leagues can configure feeder leagues that are automatically searched:
Example - English Premier League:
{
"epl": {
"feederLeagues": ["championship", "league1", "league2"]
}
}
This enables finding promoted/relegated teams without changing the league code.
Example - Tennis:
{
"tennis": {
"feederLeagues": ["atp", "wta"]
}
}
Allows unified tennis endpoint while searching both ATP and WTA rosters.
Caching Strategy
The application implements multi-layer caching to optimize performance and reduce API calls.
Team Data Cache
- Duration: 24 hours (configurable via
IMAGE_CACHE_HOURS) - Scope: Per league
- Storage: In-memory cache
- Key Format:
{league}_teams
Color Cache
- Duration: 24 hours
- Scope: Per team
- Storage: In-memory cache with size limits
- Key Format:
colors_{teamId} - Max Size: 1000 entries (oldest entries removed when exceeded)
Image Cache
- Duration: 24 hours (configurable via
IMAGE_CACHE_HOURS) - Scope: Per unique image request
- Storage: In-memory buffer cache
- Key Format: Content-based hash of parameters
Cache Invalidation
- Automatic cleanup of expired entries
- Manual cache clearing available via provider methods
- Restart server to clear all caches
Team Matching System
Normalization
Text is normalized for matching using two approaches:
- Standard Normalization:
- Remove accents and diacritical marks (e.g., “Montréal” → “Montreal”)
- Convert to lowercase
- Replace non-alphanumeric characters with spaces
- Collapse multiple spaces
- Trim whitespace
- Compact Normalization:
- Remove accents and diacritical marks
- Convert to lowercase
- Remove all non-alphanumeric characters
- No spaces (e.g., “Los Angeles” → “losangeles”)
Accent Handling: The system uses Unicode NFD normalization to strip diacritical marks, allowing teams like “Atlético Madrid”, “São Paulo”, or “Montréal” to match with or without accents.
Location Abbreviations
The system recognizes common location abbreviations:
| Abbreviation | Expansions |
|---|---|
la | Los Angeles, LA |
ny, nyc | New York, NYC |
sf | San Francisco, SF |
dc | Washington, DC |
chi | Chicago, Chi |
atl | Atlanta, Atl |
And many more. See teamUtils.js for the complete list.
Matching Scores
Teams are scored based on how well they match the input:
| Match Type | Score |
|---|---|
| Custom alias (exact) | 1000 |
| Abbreviation (exact) | 1000 |
| Team name (exact) | 900 |
| Short display name (exact) | 850 |
| Full name (exact) | 800 |
| Team name (partial contains) | 700 |
| City (exact) | 500 |
| Location+Team concatenation | 950 |
| City (partial) | 100 |
Priority Order: Team name matches are now prioritized over exact city matches (700 vs 500) to prevent false matches where a city name inadvertently matches a different team.
The team with the highest score is selected. Teams with zero score are rejected.
Special Cases
- Location prefixes: “losangelesfc” matches “LAFC” (LA + FC)
- Flexible spacing: “Man Utd” matches “manutd”
- Case insensitive: “LAKERS” matches “lakers”
- Accent insensitive: “Atlético” matches “Atletico”, “Montréal” matches “Montreal”
- Unicode normalization: All diacritical marks are stripped during matching
Route Loading System
Priority-Based Loading
Routes are loaded in a specific order to prevent conflicts between similar patterns:
- Priority Routes: Routes with an explicit
priorityfield are loaded first (lower numbers = higher priority) - Alphabetical Routes: Routes without a priority field are loaded alphabetically
Example:
module.exports = {
priority: 1, // Load before other routes
paths: ["/ncaa/:sport/:type"],
method: "get",
handler: async (req, res) => { ... }
}
Why Priority Matters
The NCAA shorthand route (/ncaa/:sport/:type) needs to be registered before the unified routes (/:league/:team1/:type) to prevent the more generic pattern from matching NCAA requests first. The priority system ensures:
- NCAA routes are registered first (priority: 1)
- Other routes load alphabetically (no priority field = lowest priority)
- No route conflicts or unexpected matching
NCAA Route Fallthrough: When the NCAA route doesn’t recognize a sport shorthand (e.g., /ncaa/team1/team2/thumb), it calls next() to pass control to the unified route handler. This allows the generic ncaa league to work for team matchups without specifying a specific sport.
Data Sources
Team and athlete data is fetched from multiple providers based on league configuration. See the Data Providers section above for details on each provider’s API endpoints and data structure.
League Logos
ESPN Leagues:
- Fetched from ESPN API or ESPN CDN
- Format:
https://a.espncdn.com/i/teamlogos/leagues/500/{league}.png
NCAA Sports:
- Hosted on NCAA.com
- Format:
https://www.ncaa.com/modules/custom/casablanca_core/img/sportbanners/{sport}.png
Custom Leagues:
- Configured via
logoUrlorlogoUrlDarkin league configuration
Rate Limiting
Configuration
- Default: 30 requests per minute per IP
- Configurable: Set
RATE_LIMIT_PER_MINUTEenvironment variable - Disable: Set
RATE_LIMIT_PER_MINUTE=0
Scope
- Image Generation: Rate limited at configured rate
- Raw Data Endpoints: 3x the image generation rate limit
- Cached Requests: Not rate limited (served from cache)
Trust Proxy
Set TRUST_PROXY to the number of proxies between the internet and your app:
- Local dev:
0 - Behind one proxy:
1 - Behind multiple proxies: Number of proxy hops
This ensures accurate IP detection for rate limiting.
Error Handling
Team Not Found
When a team is not found, the API returns:
{
"error": "Team not found: '{input}' in {LEAGUE}. Available teams: {list}"
}
Fallback Option:
Set fallback=true query parameter to handle missing teams gracefully using the multi-layer fallback system:
Single Team Endpoints:
GET /nba/invalidteam/thumb?fallback=true
Returns the NBA league thumbnail instead of an error.
Matchup Endpoints:
GET /nba/lakers/invalidteam/thumb?style=1&fallback=true
Generates the matchup using:
- Lakers logo and colors for team 1
- Greyscale league logo (35% opacity) with light grey colors (#d3d3d3, #b8b8b8) for the missing team
- The selected style and options are preserved
Performance: Both teams in a matchup are resolved in parallel. If both teams fail and need the greyscale league logo, it’s downloaded and processed only once, then reused for both teams.
Ultimate Fallback: If the league logo itself is invalid (SVG, HTML, etc.), the system generates a minimal single-letter placeholder on a transparent background as the final safety net.
This allows matchup generation to continue even when one or both teams are not found, using a subtle placeholder that clearly indicates missing data while maintaining the overall design aesthetic.
How It Works Internally:
The system uses a shared handleTeamNotFoundError() utility across all route handlers to ensure consistent fallback behavior. The resolution process tries:
Timeout Handling
- Request Timeout: 10 seconds (configurable via
REQUEST_TIMEOUT) - Server Timeout: 30 seconds (configurable via
SERVER_TIMEOUT)
Prevents hanging on slow/unresponsive external services.
Development Mode
Set NODE_ENV=development for:
- Detailed error messages
- Full stack traces in responses
- Additional logging
Performance Optimizations
Image Caching
- Content-based cache keys (same parameters = same cached image)
- In-memory storage for fast retrieval
- Automatic cleanup of expired entries
API Request Optimization
- Batch team data fetching (all teams per league in one request)
- 24-hour cache prevents repeated API calls
- Timeout handling prevents resource exhaustion
Memory Management
- Color cache has size limits (max 1000 entries)
- Automatic cleanup of oldest entries when limit exceeded
- Efficient buffer management for images
Logging
Console Logging
- Colored output (configurable via
FORCE_COLOR) - Timestamps (configurable via
SHOW_TIMESTAMP) - Request logging with response times
- Error logging with stack traces (in development mode)
File Logging
Enable with LOG_TO_FILE=true:
- Location:
./logsdirectory - Format:
app-YYYY-MM-DD-NNN.log - Rotation: ~100KB per file
- Retention: Configurable via
MAX_LOG_FILES(default: 10) - Content: Full timestamps and stack traces (always)
CORS Configuration
- Default: Allow all origins (
*) - Configurable: Set
CORS_ORIGINto specific domain - Max Age: 24 hours (configurable via
CORS_MAX_AGE)
Security Considerations
Input Validation
- League codes validated against supported list
- Team identifiers sanitized before use
- Query parameters validated and typed
Image Proxying
- Team and league logos are proxied through the server
- Prevents direct client access to external URLs
- Ensures consistent caching behavior
Rate Limiting
- Prevents abuse and resource exhaustion
- IP-based limits with proxy support
- Separate limits for different endpoint types
Future Enhancements
Potential areas for expansion:
- Additional combat sports (Boxing, Wrestling, etc.)
- More visual styles and customization options
- Image format options (JPEG, WebP, SVG)
- Persistent cache storage (Redis, filesystem)
- Advanced team matching with ML/AI
- Historical data and archive support
- Real-time game score integration
- Custom font and typography options
- Fighter statistics and rankings display