Skip to content

Repository files navigation

🎾 Tennis Scoreboard API

A comprehensive REST API with WebSocket support for managing tennis courts and receiving real-time tennis match scoring data. This API automatically processes complex tennis scoring data and provides simplified, optimized data structures for scoreboard displays.

✨ Features

  • πŸ” API Key Authentication - Secure endpoints with configurable API keys
  • 🏟️ Court Management - Create and manage tennis courts
  • πŸ“‘ Real-time WebSocket Support - Live score updates for scoreboards
  • 🎯 Smart Data Mapping - Automatically simplifies complex JSON to essential scoreboard data
  • ⚑ Optimized Performance - Reduces data size by ~85% while preserving all essential information
  • πŸ›‘οΈ Input Validation - Comprehensive request validation with detailed error messages
  • πŸ“Š Scoring Data Reception - Receive and broadcast tennis scoring data in real-time
  • πŸ“š Interactive Swagger Documentation - Complete API documentation with testing interface

πŸš€ Quick Start

Prerequisites

  • Node.js (v18 or higher)
  • Yarn package manager

Installation

  1. Clone and install dependencies:

    cd scoring-api
    yarn install
  2. Start the development server:

   yarn start:dev
  1. Verify the API is running:
    🎾 Tennis Scoreboard API is running on: http://localhost:3000
    πŸ“‘ WebSocket server is available at: ws://localhost:3000
    πŸ“š Swagger API Documentation: http://localhost:3000/api
    πŸ“‹ Complete Documentation: See README.md
    

πŸ”§ How to Use

Step 1: Get Available Courts

First, retrieve the list of available courts for user selection:

const response = await fetch('http://localhost:3000/courts', {
  headers: {
    'X-API-Key': 'sk_test_1234567890abcdef'
  }
});

const courts = await response.json();
console.log('Available courts:', courts);

Response:

[
  {
    "id": "1",
    "name": "Center Court",
    "description": "Main stadium court",
    "surfaceType": "HARD",
    "isIndoor": false,
    "isActive": true
  },
  {
    "id": "2", 
    "name": "Court 1",
    "description": "Practice court 1",
    "surfaceType": "HARD",
    "isIndoor": false,
    "isActive": true
  }
]

Step 2: Connect WebSocket for Real-time Updates

Set up WebSocket connection to receive live score updates:

import io from 'socket.io-client';

const socket = io('ws://localhost:3000');

// Join a specific match room for updates
socket.emit('join_match', { matchId: 'your-match-id' });

// Or join a court room to get updates for that court
socket.emit('join_court', { courtId: 'selected-court-id' });

// Listen for real-time score updates
socket.on('score_update', (scoreboardData) => {
  console.log('Score updated:', scoreboardData);
  updateScoreboardDisplay(scoreboardData);
});

// Acknowledgment when joining rooms
socket.on('joined_match', (data) => {
  console.log('Joined match room:', data.matchId);
});

socket.on('joined_court', (data) => {
  console.log('Joined court room:', data.courtId);
});

Step 3: Send Tennis Scoring Data

Send your complex tennis scoring data to the API (this is the main integration point):

// Your complex tennis scoring data from the scoring application
const complexTennisData = {
  "data": {
    "weather": {"sys": {"timestamp": "2025-07-11T23:07:17.778Z"}},
    "schedule": {"scheduledTime": "16:00", "scheduledDate": null, "timezoneOffset": 420},
    "_id": "68719925c5bf9840f742a23e",
    "startDate": "2025-07-11T23:08:08.291Z",
    "matchFormat": "SET3-S:6NOAD/TB7@6",
    "matchStatus": "IN_PROGRESS",
    "score": {
      "scoreStringSide1": "6-1 2-6 4-1 (0-0)",
      "scoreStringSide2": "1-6 6-2 1-4 (0-0)",
      "side1PointScore": "0",
      "side2PointScore": "0",
      // ... rest of your complex data
    },
    // ... full structure as provided
  }
};

// Send to the API
const response = await fetch('http://localhost:3000/scoring/update', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'sk_test_1234567890abcdef'
  },
  body: JSON.stringify(complexTennisData)
});

const result = await response.json();
console.log('Update result:', result);

API Response:

{
  "success": true,
  "matchId": "74794423-3c57-44e8-96a1-ce5d8954887e",
  "updatedAt": "2025-01-16T...",
  "scoreboardData": {
    "matchId": "74794423-3c57-44e8-96a1-ce5d8954887e",
    "status": "IN_PROGRESS",
    "side1Player": "Test Player",
    "side2Player": "Test Player",
    "side1Points": "0",
    "side2Points": "0",
    "sets": [
      {"setNumber": 1, "side1Score": 6, "side2Score": 1, "isCompleted": true},
      {"setNumber": 2, "side1Score": 2, "side2Score": 6, "isCompleted": true},
      {"setNumber": 3, "side1Score": 4, "side2Score": 1, "isCompleted": false}
    ],
    "servingSide": 1,
    "servingPlayer": 1,
    "format": "SET3-S:6NOAD/TB7@6",
    "startTime": "2025-07-11T23:08:08.291Z",
    "lastUpdate": "2025-01-16T..."
  }
}

Step 4: Display Scoreboard Data

Use the simplified scoreboardData to update your display:

function updateScoreboardDisplay(scoreboardData) {
  // Update player names
  document.getElementById('player1').textContent = scoreboardData.side1Player;
  document.getElementById('player2').textContent = scoreboardData.side2Player;
  
  // Update current point scores
  document.getElementById('score1').textContent = scoreboardData.side1Points;
  document.getElementById('score2').textContent = scoreboardData.side2Points;
  
  // Update set scores
  scoreboardData.sets.forEach((set, index) => {
    document.getElementById(`set${index + 1}_player1`).textContent = set.side1Score;
    document.getElementById(`set${index + 1}_player2`).textContent = set.side2Score;
  });
  
  // Highlight serving player
  const servingPlayer = scoreboardData.servingSide === 1 ? 'player1' : 'player2';
  document.querySelectorAll('.serving').forEach(el => el.classList.remove('serving'));
  document.getElementById(servingPlayer).classList.add('serving');
  
  // Update match status
  document.getElementById('status').textContent = scoreboardData.status;
}

πŸ”‘ Authentication

All API endpoints require authentication using API keys. Include your API key in request headers:

// Using X-API-Key header (recommended)
headers: {
  'X-API-Key': 'your-api-key-here'
}

// Or using Authorization header
headers: {
  'Authorization': 'Bearer your-api-key-here'
}

Default API Keys

  • Testing: sk_test_1234567890abcdef
  • Production: sk_prod_abcdef1234567890

Note: Change these default keys in production by updating src/auth/api-key.guard.ts

πŸ“Š Data Transformation & Mapping

The API automatically transforms your complex tennis scoring data:

Input: Complex JSON (~15,000+ characters) with detailed match tracking Output: Simplified scoreboard data (~2,000 characters) with essential information only

Reduction: ~85% smaller while preserving all scoreboard-relevant data

From Complex Structure:

  • Extracts player names from nested participant objects
  • Maps detailed scoring to simple point/game/set scores
  • Converts match status codes to standard enum values
  • Extracts server information
  • Simplifies set scores and tiebreak information

To Simplified Structure:

  • Clean player names and IDs
  • Standard score format (0, 15, 30, 40, AD)
  • Set scores with completion status
  • Current server information
  • Match status (NOT_STARTED, IN_PROGRESS, COMPLETED, SUSPENDED)

πŸ› οΈ Available Scripts

# Development
yarn start:dev          # Start with hot reload
yarn start:debug        # Start with debugging enabled

# Production
yarn build              # Build the application
yarn start:prod         # Start production server

# Testing
yarn test               # Run unit tests
yarn test:watch         # Run tests in watch mode
yarn test:e2e           # Run end-to-end tests

# Code Quality
yarn lint               # Run ESLint
yarn format             # Format code with Prettier

πŸ“‘ WebSocket Events

Client β†’ Server Events

  • join_match - Join a specific match room

    socket.emit('join_match', { matchId: 'match-id' });
  • join_court - Join a court room for all matches on that court

    socket.emit('join_court', { courtId: 'court-id' });
  • leave_match - Leave a match room

    socket.emit('leave_match', { matchId: 'match-id' });

Server β†’ Client Events

  • score_update - Real-time score updates with simplified scoreboard data
  • joined_match - Confirmation when joining a match room
  • joined_court - Confirmation when joining a court room
  • error - Error messages

πŸ—οΈ Project Structure

src/
β”œβ”€β”€ auth/                 # Authentication (API key guard)
β”œβ”€β”€ courts/              # Court management endpoints
β”œβ”€β”€ scoring/             # Scoring data reception & WebSocket gateway
β”œβ”€β”€ dto/                 # Data Transfer Objects & validation
β”œβ”€β”€ utils/               # Utilities (score mapping)
β”œβ”€β”€ app.module.ts        # Main application module
└── main.ts             # Application entry point

πŸ“‹ Complete API Reference

Courts Management

GET /courts

Get list of all active courts.

Response:

[
  {
    "id": "1",
    "name": "Center Court",
    "description": "Main stadium court",
    "surfaceType": "HARD",
    "isIndoor": false,
    "isActive": true,
    "createdAt": "2025-01-16T...",
    "updatedAt": "2025-01-16T..."
  }
]

POST /courts

Create a new court.

Request Body:

{
  "name": "Court 3",
  "description": "Practice court",
  "surfaceType": "CLAY",
  "isIndoor": true,
  "isActive": true
}

GET /courts/:id

Get a specific court by ID.

PUT /courts/:id

Update a court.

DELETE /courts/:id

Deactivate a court (soft delete).

Scoring Endpoints (Main Integration Point)

POST /scoring/update

This is the main endpoint for your tennis scoring application to send data.

Accepts the complex JSON structure from your tennis scoring application and automatically maps it to simplified scoreboard data.

Request Body: Send the full JSON structure as provided in your example. The API will automatically extract the essential information.

Response:

{
  "success": true,
  "matchId": "74794423-3c57-44e8-96a1-ce5d8954887e",
  "updatedAt": "2025-01-16T...",
  "scoreboardData": {
    "matchId": "74794423-3c57-44e8-96a1-ce5d8954887e",
    "status": "IN_PROGRESS",
    "side1Player": "Test Player",
    "side2Player": "Test Player",
    "side1Points": "0",
    "side2Points": "0",
    "sets": [
      {
        "setNumber": 1,
        "side1Score": 6,
        "side2Score": 1,
        "isCompleted": true
      }
    ],
    "servingSide": 1,
    "servingPlayer": 1,
    "format": "SET3-S:6NOAD/TB7@6",
    "court": "court_id",
    "startTime": "2025-07-11T23:08:08.291Z",
    "lastUpdate": "2025-01-16T..."
  }
}

GET /scoring/scoreboard/:matchId

Get simplified scoreboard data for display.

POST /scoring/test-mapping

Test endpoint to validate data mapping without updating matches.

🀝 Integration Example

Complete integration example:

class TennisScoreboardIntegration {
  constructor(apiKey, baseUrl = 'http://localhost:3000') {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
    this.socket = io(baseUrl);
  }

  async initialize() {
    // 1. Get available courts
    const courts = await this.getCourts();
    console.log('Available courts:', courts);
    
    // 2. Let user select court (implementation depends on your UI)
    const selectedCourtId = await this.showCourtSelection(courts);
    
    // 3. Connect to court's WebSocket room
    this.socket.emit('join_court', { courtId: selectedCourtId });
    
    // 4. Listen for updates
    this.socket.on('score_update', (match) => {
      this.updateScoreboard(match.scoreboardData);
    });
    
    return selectedCourtId;
  }

  async getCourts() {
    const response = await fetch(`${this.baseUrl}/courts`, {
      headers: { 'X-API-Key': this.apiKey }
    });
    return response.json();
  }

  async sendScoringData(complexTennisData) {
    const response = await fetch(`${this.baseUrl}/scoring/update`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': this.apiKey
      },
      body: JSON.stringify(complexTennisData)
    });
    return response.json();
  }

  updateScoreboard(scoreboardData) {
    // Your scoreboard update logic here
    console.log('Updating scoreboard:', scoreboardData);
  }
}

// Usage
const integration = new TennisScoreboardIntegration('sk_test_1234567890abcdef');
await integration.initialize();

⚠️ Error Handling

All endpoints return structured error responses:

{
  "success": false,
  "error": "Error message",
  "timestamp": "2025-01-16T..."
}

Common HTTP Status Codes:

  • 400: Bad Request (validation errors)
  • 401: Unauthorized (invalid API key)
  • 404: Not Found (resource doesn't exist)
  • 500: Internal Server Error

πŸ› Troubleshooting

Common Issues:

  1. 401 Unauthorized: Check your API key is correct and included in headers
  2. 404 Not Found: Verify the endpoint URL and method
  3. WebSocket not connecting: Ensure the server is running and ports are accessible
  4. Data not updating: Check that you're sending data to /scoring/update endpoint

Debug Mode:

yarn start:debug

πŸ“„ License

This project is licensed under the UNLICENSED license.


πŸ“š Interactive API Documentation

This API includes Swagger/OpenAPI documentation for interactive testing and exploration:

🌐 Access Swagger UI

Visit http://localhost:3000/api when the server is running to access the interactive documentation.

πŸ”‘ Authentication in Swagger

  1. Click the "Authorize" button in Swagger UI
  2. Enter your API key in either format:
    • API Key: sk_test_1234567890abcdef (in X-API-Key field)
    • Bearer Token: sk_test_1234567890abcdef (in Authorization field)
  3. Click "Authorize" to authenticate all requests

✨ Swagger Features

  • Interactive Testing - Test all endpoints directly from the browser
  • Request/Response Examples - See real examples for all endpoints
  • Schema Documentation - Complete data models and validation rules
  • Try It Out - Execute API calls with your data
  • Download OpenAPI Spec - Export API specification for tools

🎯 Key Endpoints in Swagger

  • Courts Management - Create, read, update, delete tennis courts
  • Scoring Integration - Main /scoring/update endpoint with examples
  • Test Endpoints - Validate your data mapping

πŸš€ Deployment

Quick Deployment

  1. Generate secure API keys:

    yarn generate:keys
  2. Set environment variables:

    export API_KEYS="sk_dev_xxx,sk_prod_xxx"
    export CORS_ORIGIN="https://your-domain.com"
  3. Deploy with Docker:

    yarn deploy

Environment Variables

Copy env.example to .env and configure:

NODE_ENV=production
PORT=3000
API_KEYS=sk_dev_your_key,sk_prod_your_key
CORS_ORIGIN=https://your-domain.com
CORS_CREDENTIALS=true

Cloud Deployment Options

  • Railway: railway up (easiest)
  • Heroku: git push heroku main
  • DigitalOcean: App Platform
  • AWS: ECS/Fargate
  • Docker: docker-compose up -d

Security Features

  • πŸ” Environment-based API keys (no hardcoded secrets)
  • πŸ›‘οΈ CORS configuration for domain security
  • 🚫 Rate limiting with nginx
  • πŸ“Š Health checks and monitoring
  • πŸ”’ HTTPS/SSL support with Let's Encrypt

For complete deployment instructions, see DEPLOYMENT.md.


Need help?

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages