A high-performance NBA statistics REST API built with Go (Fiber), PostgreSQL, and NGINX. Data is scraped from Basketball Reference and served through a load-balanced, containerized stack with built-in observability.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NGINX (reverse proxy / round-robin load balancer :8080) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β API Instance x3 (Fiber :5000 each) β
β ββββββββββββ βββββββββββββββ βββββββββββββββββββββ β
β β Routes ββ β Controllers ββ β Services (scraper) β β
β ββββββββββββ βββββββββββββββ βββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PostgreSQL 15 (GORM ORM) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Prometheus + Grafana (metrics & dashboards) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
.
βββ main.go # Entry point (API server or import-data mode)
βββ import.go # Bulk data import orchestration
βββ config/ # Database initialization
βββ models/ # GORM models (Game, PlayerAdvancedStat, PlayerTotalStat, etc.)
βββ controllers/ # HTTP handlers, DTOs, pagination, filtering, sorting
βββ routes/ # Route registration grouped by domain
βββ services/ # Web scrapers (Basketball Reference via goquery)
βββ utils/
β βββ middleware/ # Rate limiter, metrics, API key auth
β βββ metrics/ # Prometheus counter/histogram definitions
β βββ security/ # API key generation & hashing
βββ nginx/ # NGINX load balancer config
βββ prometheus/ # Prometheus scrape config
βββ grafana/ # Pre-provisioned dashboards & datasources
βββ docker-compose.yml # Production (Coolify)
βββ docker-compose.local.yml # Local development (includes Postgres)
βββ docker-compose.override.yml # Override for remote DB development
| Method | Path | Description |
|---|---|---|
| GET | /api/games |
Game data with box scores, line scores, team/player stats |
| GET | /api/playeradvancedstats |
Advanced stats (PER, WS, VORP, BPM, etc.) |
| GET | /api/playertotals |
Season totals (points, rebounds, assists, etc.) |
| GET | /api/playershotchart |
Shot chart coordinate data |
| GET | /swagger/* |
Interactive Swagger UI documentation |
| GET | /metrics |
Prometheus metrics endpoint |
| POST | /admin/keys |
Create API key (requires X-Admin-Secret header) |
| Parameter | Type | Description |
|---|---|---|
page |
int | Page number (default: 1) |
pageSize |
int | Results per page (default: 20) |
sortBy |
string | Field to sort by (varies per endpoint) |
ascending |
bool | Sort direction (default: false / descending) |
season |
int | Filter by season year (e.g., 2025) |
team |
string | Filter by team abbreviation (e.g., LAL, BOS) |
playerId |
string | Filter by player ID (e.g., jamesle01) |
isPlayoff |
bool | Filter for playoff stats |
| Parameter | Type | Description |
|---|---|---|
date |
string | Filter by date (YYYY-MM-DD) |
gameId |
string | Filter by specific game ID |
include |
string | Comma-separated associations to preload: lineScores, playerGameBasicStats, playerGameAdvStats, teamGameBasicStats, teamGameAdvStats |
# Get top scorers for the 2025 season
curl "http://localhost:8080/api/playertotals?season=2025&sortBy=points&pageSize=10"
# Get a specific game with full box score
curl "http://localhost:8080/api/games?gameId=202501010LAL&include=lineScores,playerGameBasicStats,teamGameBasicStats"
# Get LeBron's advanced stats across all seasons
curl "http://localhost:8080/api/playeradvancedstats?playerId=jamesle01&sortBy=season&ascending=true"
# Get shot chart data for Curry in 2024
curl "http://localhost:8080/api/playershotchart?playerId=curryst01&season=2024"All endpoints return paginated JSON:
{
"data": [...],
"pagination": {
"total": 450,
"page": 1,
"pageSize": 20,
"pages": 22
}
}The API enforces a per-IP rate limit of 20 requests per minute per instance. With 3 instances behind NGINX round-robin, the effective limit is ~60 requests/minute per client.
Exceeding the limit returns:
HTTP 429
{"error": "Rate limit exceeded. Try again later."}- Docker & Docker Compose
- Go 1.23+ (for local development)
- A
.envfile with database credentials
DB_HOST=postgres
DB_USER=your_user
DB_PASSWORD=your_password
DB_NAME=your_db
DB_PORT=5432
ADMIN_SECRET=your_admin_secret# Start everything (Postgres, 3 API instances, NGINX, Prometheus, Grafana)
docker-compose -f docker-compose.local.yml up --build -d
# Or use the Makefile shortcut
make upServices will be available at:
| Service | URL |
|---|---|
| API (via NGINX) | http://localhost:8081 |
| Prometheus | http://localhost:9090 |
| Grafana | http://localhost:3001 (admin/testing) |
| API instance 1 (direct) | http://localhost:5001 |
| API instance 2 (direct) | http://localhost:5002 |
| API instance 3 (direct) | http://localhost:5003 |
The application has a dual-mode entry point. To run the initial data import (migrations + scraping):
docker-compose -f docker-compose.local.yml run --rm db-initThis runs main.go with the import-data argument, which:
- Runs all GORM AutoMigrate operations
- Scrapes Basketball Reference for player advanced stats, totals, game schedules, and box scores
- Upserts all data into PostgreSQL
docker compose down
# or
make downThe main docker-compose.yml is configured for deployment on Coolify with an external coolify network. It expects the database to be provisioned separately (no local Postgres service).
The docker-compose.override.yml disables the local Postgres container and removes depends_on constraints, allowing API services to connect to a remote database specified in .env.
Exposed at /metrics on each API instance. Tracked metrics:
nba_http_requests_totalβ counter by method, endpoint, statusnba_http_request_duration_secondsβ histogram by method, endpointnba_db_operations_totalβ counter by operation, entity
Pre-provisioned dashboards visualize request rates and endpoint usage. Access at port 3001 (local) or 3000 (production).
API key authentication is available but currently disabled. To create keys for future use:
# Create a key
curl -XPOST http://localhost:8080/admin/keys \
-H "X-Admin-Secret: $ADMIN_SECRET" \
-d '{"label":"my-app"}'
# β {"id":1, "apiKey":"ab12cdβ¦"}
# Revoke a key
curl -XPOST http://localhost:8080/admin/keys/1/revoke \
-H "X-Admin-Secret: $ADMIN_SECRET"To enforce API keys, uncomment app.Use(middleware.APIKeyAuth(db)) in main.go.
swag init -g main.go -o docsgo test -v .cd test
go run loadtest.go -n 100 -c 10 -url "http://localhost:8080/api/playeradvancedstats?page=1&pageSize=20" -log results.log| Component | Technology |
|---|---|
| Language | Go 1.23+ |
| Framework | Fiber v2 |
| ORM | GORM |
| Database | PostgreSQL 15 |
| Scraping | goquery |
| Load Balancer | NGINX |
| Monitoring | Prometheus + Grafana |
| Docs | Swagger (swaggo) |
| Containerization | Docker + Docker Compose |