/**
* NHL API Client
*
* Interfaces with the official NHL APIs:
* - api-web.nhle.com (primary data source)
* - api.nhle.com/stats/rest (statistics)
*/
import fetch from 'node-fetch';
const NHL_API_BASE = 'https://api-web.nhle.com/v1';
const NHL_STATS_API_BASE = 'https://api.nhle.com/stats/rest/en';
export interface GameScore {
id: number;
season: number;
gameType: number;
gameDate: string;
venue: string;
homeTeam: {
id: number;
abbrev: string;
name: string;
score: number;
};
awayTeam: {
id: number;
abbrev: string;
name: string;
score: number;
};
gameState: string;
period: number;
periodDescriptor?: {
number: number;
periodType: string;
};
}
export interface TeamStandings {
teamAbbrev: {
default: string;
};
teamName: {
default: string;
};
teamLogo: string;
wins: number;
losses: number;
otLosses: number;
points: number;
gamesPlayed: number;
goalFor: number;
goalAgainst: number;
goalDifferential: number;
regulationWins: number;
winPctg: number;
conferenceAbbrev: string;
divisionAbbrev: string;
placeName?: {
default: string;
};
}
export interface PlayerStats {
id: number;
firstName: {
default: string;
};
lastName: {
default: string;
};
sweaterNumber: number;
headshot: string;
teamAbbrev: string;
teamName: {
default: string;
};
teamLogo: string;
position: string;
value: number; // The stat value (points, goals, assists, etc.)
}
export interface GoalieStats {
id: number;
firstName: {
default: string;
};
lastName: {
default: string;
};
sweaterNumber: number;
headshot: string;
teamAbbrev: string;
teamName: {
default: string;
};
teamLogo: string;
position: string;
value: number; // The stat value (savePctg, wins, etc.)
}
export class NHLAPIClient {
private async fetchJSON(url: string): Promise<any> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`NHL API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
/**
* Get today's NHL games with scores and status
*/
async getTodaysScores(date?: string): Promise<GameScore[]> {
const dateStr = date || new Date().toISOString().split('T')[0];
const data = await this.fetchJSON(`${NHL_API_BASE}/score/${dateStr}`);
return data.games || [];
}
/**
* Get detailed game data including play-by-play
*/
async getGameDetails(gameId: number): Promise<any> {
return this.fetchJSON(`${NHL_API_BASE}/gamecenter/${gameId}/play-by-play`);
}
/**
* Get game boxscore with detailed stats
*/
async getGameBoxscore(gameId: number): Promise<any> {
return this.fetchJSON(`${NHL_API_BASE}/gamecenter/${gameId}/boxscore`);
}
/**
* Get current standings (optionally by date)
*/
async getStandings(date?: string): Promise<{ standings: TeamStandings[] }> {
const dateStr = date || new Date().toISOString().split('T')[0];
return this.fetchJSON(`${NHL_API_BASE}/standings/${dateStr}`);
}
/**
* Get standings by season
*/
async getStandingsBySeason(season: string): Promise<{ standings: TeamStandings[] }> {
return this.fetchJSON(`${NHL_API_BASE}/standings/${season}`);
}
/**
* Get team schedule
*/
async getTeamSchedule(teamAbbrev: string, season?: string): Promise<any> {
const seasonStr = season || this.getCurrentSeason();
return this.fetchJSON(`${NHL_API_BASE}/club-schedule-season/${teamAbbrev}/${seasonStr}`);
}
/**
* Get schedule for a specific week
*/
async getSchedule(date?: string): Promise<any> {
const dateStr = date || new Date().toISOString().split('T')[0];
return this.fetchJSON(`${NHL_API_BASE}/schedule/${dateStr}`);
}
/**
* Get player stats for current season
*/
async getPlayerStats(playerId: number, season?: string): Promise<any> {
const seasonStr = season || this.getCurrentSeason();
return this.fetchJSON(`${NHL_API_BASE}/player/${playerId}/landing`);
}
/**
* Get top skaters by category
*/
async getTopSkaters(category: string = 'points', limit: number = 20, season?: string): Promise<PlayerStats[]> {
const seasonStr = season || this.getCurrentSeason();
if (!season) {
// Use current stats leaders endpoint
const data = await this.fetchJSON(
`${NHL_API_BASE}/skater-stats-leaders/current?categories=${category}&limit=${limit}`
);
return data[category] || [];
} else {
// Use seasonal stats leaders endpoint
const data = await this.fetchJSON(
`${NHL_API_BASE}/skater-stats-leaders/${seasonStr}/2?categories=${category}&limit=${limit}`
);
return data[category] || [];
}
}
/**
* Get top goalies
*/
async getTopGoalies(limit: number = 20, season?: string): Promise<GoalieStats[]> {
const seasonStr = season || this.getCurrentSeason();
const category = 'savePctg'; // Default to save percentage
if (!season) {
// Use current stats leaders endpoint
const data = await this.fetchJSON(
`${NHL_API_BASE}/goalie-stats-leaders/current?categories=${category}&limit=${limit}`
);
return data[category] || [];
} else {
// Use seasonal stats leaders endpoint
const data = await this.fetchJSON(
`${NHL_API_BASE}/goalie-stats-leaders/${seasonStr}/2?categories=${category}&limit=${limit}`
);
return data[category] || [];
}
}
/**
* Get playoff bracket (when available)
*/
async getPlayoffBracket(season?: string): Promise<any> {
const year = season || new Date().getFullYear().toString();
return this.fetchJSON(`${NHL_API_BASE}/playoff-bracket/${year}`);
}
/**
* Get playoff series
*/
async getPlayoffSeries(season?: string): Promise<any> {
const seasonStr = season || this.getCurrentSeason();
return this.fetchJSON(`${NHL_API_BASE}/playoff-series/${seasonStr}/playoff`);
}
/**
* Get team roster and stats
*/
async getTeamStats(teamAbbrev: string, season?: string): Promise<any> {
const seasonStr = season || this.getCurrentSeason();
return this.fetchJSON(`${NHL_API_BASE}/club-stats/${teamAbbrev}/${seasonStr}/2`);
}
/**
* Get team roster
*/
async getTeamRoster(teamAbbrev: string, season?: string): Promise<any> {
const seasonStr = season || this.getCurrentSeason();
return this.fetchJSON(`${NHL_API_BASE}/roster/${teamAbbrev}/${seasonStr}`);
}
/**
* Helper: Get current NHL season ID (e.g., 20242025)
*/
private getCurrentSeason(): string {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1; // 0-indexed
// NHL season typically starts in October
if (month >= 10) {
return `${year}${year + 1}`;
} else {
return `${year - 1}${year}`;
}
}
/**
* Parse season string to readable format
*/
formatSeason(season: string): string {
if (season.length === 8) {
const startYear = season.substring(0, 4);
const endYear = season.substring(4, 8);
return `${startYear}-${endYear}`;
}
return season;
}
}