Creando un API REST para Estadísticas de Basket-ball con PHP, MySQL y PDO

En esta entrada del blog vamos a detallar un tutorial de programación web, vamos a construir de la manera más fácil posible, un paso a paso para crear un API RESTful para gestionar estadísticas de partidos de baloncesto utilizando PHP, MySQL y PDO con programación orientada a objetos. Si quieren conocer lo básico de la arquitectura REST, pueden leer la entrada del blog al respecto. El API permitirá realizar operaciones CRUD (Crear, Leer, Actualizar y Eliminar) sobre las estadísticas de los jugadores. La siguiente imagen ejemplifica como funcionaría nuestro pequeño proyecto REST.

¿Qué necesitamos para comenzar?

  • Servidor web (Apache, Nginx)
  • PHP 7.4 o superior
  • MySQL 5.7 o superior
  • Conocimientos básicos de PHP y MySQL

Comenzaremos creado la estructura de nuestro proyecto, pueden utilizar el editor de código de su preferencia, en este caso yo utilizo Visual Studio Code. La estructura del proyecto quedaría de la siguiente forma:
  /basketball-api
    /config
        Database.php
    /models
        PlayerStats.php
    /controllers
        StatsController.php
    index.php
    .htaccess 

Como pueden observar, la estructura de trabajo es básicamente considerando el patrón Model-View-Controller, en la siguiente entrada del blog explico de que va esto. Ahora veremos paso a paso como crear nuestra API REST. 

INICIAMOS

Paso 1: Configurar la base de datos. Primero, creemos nuestra tabla para almacenar las estadísticas, recordemos que estamos trabajando con MySQL. Crearemos una base de datos con el nombre de "basketball_stats", en ella crearemos una entidad (tabla) con el nombre de "player_stats" con ocho campos: id, player_name, points_scored, three_points_made, fouls_committed, game_date, created_at, updated_at.
CREATE DATABASE basketball_stats;

USE basketball_stats;

CREATE TABLE player_stats (
    id INT AUTO_INCREMENT PRIMARY KEY,
    player_name VARCHAR(100) NOT NULL,
    points_scored INT NOT NULL,
    three_points_made INT NOT NULL,
    fouls_committed INT NOT NULL,
    game_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Paso 2. Configuración de la conexión PDO. Aquí crearemos el archivo config/Database.php. Este archivo es simplemente una clase para manejar la conexión a nuestra base de datos. Contendrá un método para la conexión, el cual colocaremos dentro de un try-catch, ahí realizaremos una conexión mediante PDO (PHP- Data Object). Considera que deberás ajustar las credenciales según tu configuración de conexión.

class Database {
    // Configuración de la base de datos
    private $host = 'localhost';
    private $db_name = 'basketball_stats';
    private $username = 'root';
    private $password = '';
    private $conn;

    // Método para conectar a la base de datos
    public function connect() {
        $this->conn = null;

        try {
            $this->conn = new PDO(
                'mysql:host=' . $this->host . ';dbname=' . $this->db_name,
                $this->username,
                $this->password
            );
            $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch(PDOException $e) {
            echo 'Connection Error: ' . $e->getMessage();
        }

        return $this->conn;
    }
}
Paso 3: Crear el modelo PlayerStats. En models/PlayerStats.php, crearemos nuestro modelo, recordemos que le llamamos modelo porque el modelo se refiere a la parte de la aplicación que maneja la lógica de datos y la interacción con las fuentes de información, como bases de datos, servicios web o cualquier otro recurso que provea o almacene datos. Formalmente, el modelo es la capa de datos y lógica de negocio dentro del patrón MVC, separada de la presentación (vista) y la interacción del usuario (controlador). Este modelo contiene todos los métodos necesarios para las operaciones CRUD: create(), read(), readOne(), update() y delete().
class PlayerStats {
    // Conexión a la base de datos y nombre de la tabla
    private $conn;
    private $table_name = 'player_stats';

    // Propiedades del objeto
    public $id;
    public $player_name;
    public $points_scored;
    public $three_points_made;
    public $fouls_committed;
    public $game_date;
    public $created_at;
    public $updated_at;

    // Constructor con conexión a la base de datos
    public function __construct($db) {
        $this->conn = $db;
    }

    // Crear una nueva estadística
    public function create() {
        // Consulta SQL para insertar un registro
        $query = 'INSERT INTO ' . $this->table_name . '
            SET
                player_name = :player_name,
                points_scored = :points_scored,
                three_points_made = :three_points_made,
                fouls_committed = :fouls_committed,
                game_date = :game_date';

        // Preparamos la consulta
        $stmt = $this->conn->prepare($query);

        // Limpiamos y vinculamos los parámetros
        $this->player_name = htmlspecialchars(strip_tags($this->player_name));
        $this->points_scored = htmlspecialchars(strip_tags($this->points_scored));
        $this->three_points_made = htmlspecialchars(strip_tags($this->three_points_made));
        $this->fouls_committed = htmlspecialchars(strip_tags($this->fouls_committed));
        $this->game_date = htmlspecialchars(strip_tags($this->game_date));

        $stmt->bindParam(':player_name', $this->player_name);
        $stmt->bindParam(':points_scored', $this->points_scored);
        $stmt->bindParam(':three_points_made', $this->three_points_made);
        $stmt->bindParam(':fouls_committed', $this->fouls_committed);
        $stmt->bindParam(':game_date', $this->game_date);

        // Ejecutamos la consulta
        if ($stmt->execute()) {
            return true;
        }

        // Si hay error, mostramos el mensaje
        printf("Error: %s.\n", $stmt->error);
        return false;
    }

    // Leer todas las estadísticas
    public function read() {
        // Consulta SQL
        $query = 'SELECT * FROM ' . $this->table_name . ' ORDER BY game_date DESC';

        // Preparamos la consulta
        $stmt = $this->conn->prepare($query);

        // Ejecutamos
        $stmt->execute();

        return $stmt;
    }

    // Leer una sola estadística por ID
    public function readOne() {
        // Consulta SQL
        $query = 'SELECT * FROM ' . $this->table_name . ' WHERE id = ? LIMIT 0,1';

        // Preparamos la consulta
        $stmt = $this->conn->prepare($query);

        // Vinculamos el parámetro ID
        $stmt->bindParam(1, $this->id);

        // Ejecutamos
        $stmt->execute();

        // Obtenemos la fila
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        // Asignamos valores a las propiedades del objeto
        if ($row) {
            $this->player_name = $row['player_name'];
            $this->points_scored = $row['points_scored'];
            $this->three_points_made = $row['three_points_made'];
            $this->fouls_committed = $row['fouls_committed'];
            $this->game_date = $row['game_date'];
        }
    }

    // Actualizar una estadística
    public function update() {
        // Consulta SQL
        $query = 'UPDATE ' . $this->table_name . '
            SET
                player_name = :player_name,
                points_scored = :points_scored,
                three_points_made = :three_points_made,
                fouls_committed = :fouls_committed,
                game_date = :game_date
            WHERE
                id = :id';

        // Preparamos la consulta
        $stmt = $this->conn->prepare($query);

        // Limpiamos y vinculamos los parámetros
        $this->player_name = htmlspecialchars(strip_tags($this->player_name));
        $this->points_scored = htmlspecialchars(strip_tags($this->points_scored));
        $this->three_points_made = htmlspecialchars(strip_tags($this->three_points_made));
        $this->fouls_committed = htmlspecialchars(strip_tags($this->fouls_committed));
        $this->game_date = htmlspecialchars(strip_tags($this->game_date));
        $this->id = htmlspecialchars(strip_tags($this->id));

        $stmt->bindParam(':player_name', $this->player_name);
        $stmt->bindParam(':points_scored', $this->points_scored);
        $stmt->bindParam(':three_points_made', $this->three_points_made);
        $stmt->bindParam(':fouls_committed', $this->fouls_committed);
        $stmt->bindParam(':game_date', $this->game_date);
        $stmt->bindParam(':id', $this->id);

        // Ejecutamos
        if ($stmt->execute()) {
            return true;
        }

        return false;
    }

    // Eliminar una estadística
    public function delete() {
        // Consulta SQL
        $query = 'DELETE FROM ' . $this->table_name . ' WHERE id = :id';

        // Preparamos la consulta
        $stmt = $this->conn->prepare($query);

        // Limpiamos y vinculamos el parámetro ID
        $this->id = htmlspecialchars(strip_tags($this->id));
        $stmt->bindParam(':id', $this->id);

        // Ejecutamos
        if ($stmt->execute()) {
            return true;
        }

        return false;
    }
}

El modelo PlayerStats es como un intermediario entre la base de datos y el controlador. Básicamente, tiene tres funciones principales, estas son:

Representar los datos de un jugador de baloncesto:
  • player_name (nombre del jugador)
  • points_scored (puntos anotados)
  • three_points_made (triples encestados)
  • fouls_committed (faltas cometidas)
  • game_date (fecha del partido)

Gestionar las operaciones con la base de datos:

  • create() → Guarda una nueva estadística.
  • read() → Obtiene todas las estadísticas.
  • readOne() → Busca una estadística por su ID.
  • update() → Modifica una estadística existente.
  • delete() → Elimina una estadística.

Validar y limpiar datos antes de guardarlos (evitando inyección SQL o datos corruptos).

Paso 4. Crear el controlador StatsController. El controlador es la parte encargada de gestionar la interacción entre el modelo y la vista, actuando como un intermediario entre estos dos componentes. Su principal responsabilidad es procesar las entradas del usuario, tomar las decisiones adecuadas y luego actualizar tanto el modelo como la vista según sea necesario. El controlador manejará todas las solicitudes HTTP (GET, POST, PUT, DELETE) y las dirige a los métodos correspondientes del modelo. En controllers/StatsController.php colocaremos el siguiente código fuente:

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

require_once '../config/Database.php';
require_once '../models/PlayerStats.php';

class StatsController {
    private $db;
    private $playerStats;

    public function __construct() {
        $database = new Database();
        $this->db = $database->connect();
        $this->playerStats = new PlayerStats($this->db);
    }

    // Procesar la solicitud
    public function processRequest() {
        $method = $_SERVER['REQUEST_METHOD'];
        $id = isset($_GET['id']) ? $_GET['id'] : null;

        switch ($method) {
            case 'GET':
                if ($id) {
                    $this->getStat($id);
                } else {
                    $this->getAllStats();
                }
                break;
            case 'POST':
                $this->createStat();
                break;
            case 'PUT':
                if ($id) {
                    $this->updateStat($id);
                }
                break;
            case 'DELETE':
                if ($id) {
                    $this->deleteStat($id);
                }
                break;
            default:
                http_response_code(405);
                echo json_encode(["message" => "Método no permitido"]);
                break;
        }
    }

    // Obtener todas las estadísticas
    private function getAllStats() {
        $stmt = $this->playerStats->read();
        $num = $stmt->rowCount();

        if ($num > 0) {
            $stats_arr = array();
            $stats_arr["records"] = array();

            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                extract($row);

                $stat_item = array(
                    "id" => $id,
                    "player_name" => $player_name,
                    "points_scored" => $points_scored,
                    "three_points_made" => $three_points_made,
                    "fouls_committed" => $fouls_committed,
                    "game_date" => $game_date,
                    "created_at" => $created_at,
                    "updated_at" => $updated_at
                );

                array_push($stats_arr["records"], $stat_item);
            }

            http_response_code(200);
            echo json_encode($stats_arr);
        } else {
            http_response_code(404);
            echo json_encode(["message" => "No se encontraron estadísticas."]);
        }
    }

    // Obtener una sola estadística
    private function getStat($id) {
        $this->playerStats->id = $id;
        $this->playerStats->readOne();

        if ($this->playerStats->player_name != null) {
            $stat_arr = array(
                "id" => $this->playerStats->id,
                "player_name" => $this->playerStats->player_name,
                "points_scored" => $this->playerStats->points_scored,
                "three_points_made" => $this->playerStats->three_points_made,
                "fouls_committed" => $this->playerStats->fouls_committed,
                "game_date" => $this->playerStats->game_date,
                "created_at" => $this->playerStats->created_at,
                "updated_at" => $this->playerStats->updated_at
            );

            http_response_code(200);
            echo json_encode($stat_arr);
        } else {
            http_response_code(404);
            echo json_encode(["message" => "Estadística no encontrada."]);
        }
    }

    // Crear nueva estadística
    private function createStat() {
        $data = json_decode(file_get_contents("php://input"));

        if (
            !empty($data->player_name) &&
            isset($data->points_scored) &&
            isset($data->three_points_made) &&
            isset($data->fouls_committed)
        ) {
            $this->playerStats->player_name = $data->player_name;
            $this->playerStats->points_scored = $data->points_scored;
            $this->playerStats->three_points_made = $data->three_points_made;
            $this->playerStats->fouls_committed = $data->fouls_committed;
            $this->playerStats->game_date = isset($data->game_date) ? $data->game_date : date('Y-m-d H:i:s');

            if ($this->playerStats->create()) {
                http_response_code(201);
                echo json_encode(["message" => "Estadística creada correctamente."]);
            } else {
                http_response_code(503);
                echo json_encode(["message" => "No se pudo crear la estadística."]);
            }
        } else {
            http_response_code(400);
            echo json_encode(["message" => "Datos incompletos."]);
        }
    }

    // Actualizar estadística
    private function updateStat($id) {
        $data = json_decode(file_get_contents("php://input"));

        $this->playerStats->id = $id;
        $this->playerStats->readOne();

        if ($this->playerStats->player_name == null) {
            http_response_code(404);
            echo json_encode(["message" => "Estadística no encontrada."]);
            return;
        }

        $this->playerStats->player_name = isset($data->player_name) ? $data->player_name : $this->playerStats->player_name;
        $this->playerStats->points_scored = isset($data->points_scored) ? $data->points_scored : $this->playerStats->points_scored;
        $this->playerStats->three_points_made = isset($data->three_points_made) ? $data->three_points_made : $this->playerStats->three_points_made;
        $this->playerStats->fouls_committed = isset($data->fouls_committed) ? $data->fouls_committed : $this->playerStats->fouls_committed;
        $this->playerStats->game_date = isset($data->game_date) ? $data->game_date : $this->playerStats->game_date;

        if ($this->playerStats->update()) {
            http_response_code(200);
            echo json_encode(["message" => "Estadística actualizada correctamente."]);
        } else {
            http_response_code(503);
            echo json_encode(["message" => "No se pudo actualizar la estadística."]);
        }
    }

    // Eliminar estadística
    private function deleteStat($id) {
        $this->playerStats->id = $id;
        $this->playerStats->readOne();

        if ($this->playerStats->player_name == null) {
            http_response_code(404);
            echo json_encode(["message" => "Estadística no encontrada."]);
            return;
        }

        if ($this->playerStats->delete()) {
            http_response_code(200);
            echo json_encode(["message" => "Estadística eliminada correctamente."]);
        } else {
            http_response_code(503);
            echo json_encode(["message" => "No se pudo eliminar la estadística."]);
        }
    }
}

El controlador en este ejemplo (StatsController) actúa como el intermediario entre las solicitudes HTTP (que llegan al API) y el modelo (PlayerStats) que interactúa con la base de datos. Su función principal es procesar las solicitudes entrantes, para lo cual detecta el método HTTP (GET, POST, PUT, DELETE) y extrae parámetros como el ID (si está presente en la URL) para poder decidir qué acción ejecutar según el tipo de solicitud.

Cada método del controlador maneja una operación específica:

  • getAllStats() (GET sin ID): Llama al método read() del modelo. Devuelve un JSON con todas las estadísticas almacenadas.
  • getStat($id) (GET con ID). Busca una sola estadística usando el ID. Si no existe, devuelve un error 404 (Not Found).
  • createStat() (POST). Lee los datos del cuerpo de la solicitud (php://input). Valida que los campos obligatorios estén presentes. Si todo es correcto, guarda los datos en la base de datos y devuelve 201 (Created).
  • updateStat($id) (PUT). Actualiza solo los campos proporcionados (el resto se mantienen igual). Si el ID no existe, devuelve 404 (Not Found).
  • deleteStat($id) (DELETE). Elimina una estadística por su ID. Si no existe, devuelve 404 (Not Found).

Paso 5. Configurar el archivo principal index.php. Este es el punto de entrada de nuestra API que instancia el controlador y procesa la solicitud.
require_once 'controllers/StatsController.php';

$controller = new StatsController();
$controller->processRequest();

Paso 6. Configurar .htaccess para URL amigables. Este archivo redirige todas las solicitudes a index.php para un enrutamiento limpio.

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
Probar nuestra API REST. Nuestra API REST se puede probar con herramientas como Postman o cURL. Para fines de ejemplo, vamos a probar nuestra API haciendo un POST con una nueva estadística:
URL: http://localhost/basketball-api/
Método: POST
Body (raw JSON):
{
    "player_name": "LeBron James",
    "points_scored": 28,
    "three_points_made": 4,
    "fouls_committed": 2
}
Ahora obtendremos todas las estadísticas (GET).

URL: http://localhost/basketball-api/
Método: GET
JSON generado por esta API :
{
  "records": [
    {
      "id": 1,
      "player_name": "LeBron James",
      "points_scored": 28,
      "three_points_made": 4,
      "fouls_committed": 2,
      "game_date": "2023-11-15 14:30:00",
      "created_at": "2023-11-15 15:45:23",
      "updated_at": "2023-11-15 15:45:23"
    },
    {
      "id": 2,
      "player_name": "Stephen Curry",
      "points_scored": 35,
      "three_points_made": 7,
      "fouls_committed": 1,
      "game_date": "2023-11-16 20:15:00",
      "created_at": "2023-11-16 21:30:10",
      "updated_at": "2023-11-16 21:30:10"
    }
  ]
}
Obtener una estadística específica (GET), agregamos el id con el número de estadística que queremos obtener.
URL: http://localhost/basketball-api/?id=1
Método: GET 
Respuesta al obtener una estadística específica (GET /?id=1)
{
  "id": 1,
  "player_name": "LeBron James",
  "points_scored": 28,
  "three_points_made": 4,
  "fouls_committed": 2,
  "game_date": "2023-11-15 14:30:00",
  "created_at": "2023-11-15 15:45:23",
  "updated_at": "2023-11-15 15:45:23"
}
Actualizar una estadística (PUT).
  URL: http://localhost/basketball-api/?id=1
  Método: PUT
  Body (raw JSON):
  {
      "points_scored": 32,
      "three_points_made": 5
  }
Eliminar una estadística (DELETE).
URL: http://localhost/basketball-api/?id=1
Método: DELETE

Respuesta al eliminar una estadística.


{
  "message": "Estadística eliminada correctamente."
}

En este tutorial hemos desarrollado un API RESTful completo para gestionar estadísticas de baloncesto utilizando PHP, MySQL y PDO con programación orientada a objetos, implementando un sistema seguro de conexión a base de datos con PDO, un modelo que representa la entidad PlayerStats, un controlador para manejar solicitudes HTTP, operaciones CRUD completas, validación básica de datos y respuestas JSON con códigos de estado HTTP apropiados. Esta API puede ampliarse con funcionalidades adicionales como autenticación, paginación, filtrado avanzado o relaciones con otras tablas (como equipos o partidos), entre otras mejoras, para adaptarse a necesidades más complejas en un entorno de producción.

Es cuánto.

Si quieres citar este artículo en tu texto, documento, etc. puedes hacerlo de la siguiente forma:

Aguilar-Calderón, J. A. (27 de marzo de 2025). Creando un API REST para Estadísticas de Basket-ball con PHP, MySQL y PDO. ANOVA LAB MX. https://anovalabmx.blogspot.com/2025/03/creando-un-api-rest-para-estadisticas.html

Comentarios

Mi foto
José Alfonso Aguilar
Mazatlán, Sinaloa, Mexico
Me gusta aprender y escribir sobre tecnología y desarrollo. Soy Ingeniero en Sistemas Computacionales, trabajo como Profesor-Investigador en la Facultad de Informática Mazatlán, de la Universidad Autónoma de Sinaloa. México. Me gusta combinar la docencia-investigación con el giro profesional del desarrollo de software y gestión de proyectos de innovación.

Entradas más populares de este blog

Historia y versiones de HTML (HyperText Markup Language)

Todo lo que debes saber sobre el Model-View Controller (MVC) para Aplicaciones Web

Stateful vs Stateless Design - ¿Cuál es la diferencia?