Winxer
Creación de un API RESTFUL GET, POST, PUT, DELETE en Vue.js 3 y PHP: Segunda Parte

Creación de un API RESTFUL GET, POST, PUT, DELETE en Vue.js 3 y PHP: Segunda Parte

Desarrollando los Modelos, Controladores y Enrutamiento en PHP para nuestro sistema de usuarios.

En la [primera parte] de este artículo, dejamos listo nuestro entorno de desarrollo: creamos el proyecto en Vue.js 3, configuramos la base de datos en MySQL y escribimos la clase de conexión `Database` usando PDO dentro de nuestra estructura MVC.

Hoy nos meteremos de lleno en el código del backend. Vamos a desarrollar el modelo de usuario, el controlador que gestionará las peticiones HTTP (`GET`, `POST`, `PUT`, `DELETE`), y configuraremos el enrutador principal para que nuestra API RESTful responda correctamente a las solicitudes del cliente.

models/user.php

Este archivo representa el **Modelo** en nuestro patrón MVC. Se encargará exclusivamente de interactuar con la tabla `users` de la base de datos. Aquí escribiremos las consultas utilizando *Prepared Statements* (consultas preparadas) de PDO para garantizar que nuestra API sea segura contra inyecciones SQL.

Crea o abre el archivo `backend/models/user.php` y añade el siguiente código:

 

<?php
class User
{
    // Conexión de base de datos y nombre de la tabla
    private $conn;
    private $table_name = "users";

    // Propiedades del objeto Usuario
    public $id;
    public $name;
    public $email;
    public $password;
    public $created_at;
    public $updated_at;

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

    // 1. OBTENER TODOS LOS USUARIOS (GET)
    public function read()
    {
        $query = "SELECT id, name, email, created_at, updated_at FROM " . $this->table_name . " ORDER BY created_at DESC";
        $stmt = $this->conn->prepare($query);
        $stmt->execute();
        return $stmt;
    }

    // 2. OBTENER UN USUARIO POR ID (GET con parámetro)
    public function readOne()
    {
        $query = "SELECT id, name, email, created_at, updated_at FROM " . $this->table_name . " WHERE id = ? LIMIT 0,1";
        $stmt = $this->conn->prepare($query);
        
        // Vinculamos el ID (primer parámetro '?')
        $stmt->bindParam(1, $this->id);
        $stmt->execute();

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

        if ($row) {
            $this->name = $row['name'];
            $this->email = $row['email'];
            $this->created_at = $row['created_at'];
            $this->updated_at = $row['updated_at'];
            return true;
        }
        return false;
    }

    // 3. CREAR UN NUEVO USUARIO (POST)
    public function create()
    {
        $query = "INSERT INTO " . $this->table_name . " SET name=:name, email=:email, password=:password";
        $stmt = $this->conn->prepare($query);

        // Limpiar y desinfectar datos básicos
        $this->name = htmlspecialchars(strip_tags($this->name));
        $this->email = htmlspecialchars(strip_tags($this->email));
        $this->password = htmlspecialchars(strip_tags($this->password));

        // Encriptar la contraseña por seguridad antes de guardarla
        $hashed_password = password_hash($this->password, PASSWORD_BCRYPT);

        // Vincular los parámetros con nombre
        $stmt->bindParam(":name", $this->name);
        $stmt->bindParam(":email", $this->email);
        $stmt->bindParam(":password", $hashed_password);

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

    // 4. ACTUALIZAR UN USUARIO (PUT)
    public function update()
    {
        // Query dinámico: actualiza contraseña solo si se envía una nueva
        $query = "UPDATE " . $this->table_name . " SET name = :name, email = :email" . 
                 ($this->password ? ", password = :password" : "") . " WHERE id = :id";

        $stmt = $this->conn->prepare($query);

        $this->name = htmlspecialchars(strip_tags($this->name));
        $this->email = htmlspecialchars(strip_tags($this->email));
        $this->id = htmlspecialchars(strip_tags($this->id));

        $stmt->bindParam(':name', $this->name);
        $stmt->bindParam(':email', $this->email);
        $stmt->bindParam(':id', $this->id);

        if ($this->password) {
            $this->password = htmlspecialchars(strip_tags($this->password));
            $hashed_password = password_hash($this->password, PASSWORD_BCRYPT);
            $stmt->bindParam(':password', $hashed_password);
        }

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

    // 5. ELIMINAR UN USUARIO (DELETE)
    public function delete()
    {
        $query = "DELETE FROM " . $this->table_name . " WHERE id = ?";
        $stmt = $this->conn->prepare($query);

        $this->id = htmlspecialchars(strip_tags($this->id));
        $stmt->bindParam(1, $this->id);

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

 

views/userView.php

En una API RESTful, la **Vista** no es un archivo HTML, sino el encargado de estructurar y emitir las respuestas en formato **JSON**, junto con las cabeceras HTTP correspondientes (como el código de estado `200 OK`, `201 Created` o `400 Bad Request`).

Escribe lo siguiente en `backend/views/userView.php`:

<?php
class UserView
{
    // Método estático para responder de forma genérica con JSON y código de estado
    public static function response($code, $data)
    {
        // Permitir peticiones desde cualquier origen (CORS) y formato JSON
        header("Access-Control-Allow-Origin: *");
        header("Content-Type: application/json; charset=UTF-8");
        header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
        header("Access-Control-Max-Age: 3600");
        header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

        http_response_code($code);
        echo json_encode($data);
        exit();
    }
}

 

controllers/userController.php

El **Controlador** recibirá las solicitudes procesadas desde el punto de entrada, capturará los datos JSON enviados por el cliente (mediante Vue.js), invocará los métodos del Modelo que acabamos de crear y le pedirá a la Vista que renderice la respuesta.

Escribe lo siguiente en `backend/controllers/userController.php`:

<?php
require_once __DIR__ . '/../models/user.php';
require_once __DIR__ . '/../views/userView.php';

class UserController
{
    private $db;
    private $user;

    public function __construct($dbConnection)
    {
        $this->db = $dbConnection;
        $this->user = new User($this->db);
    }

    // Manejar peticiones GET
    public function handleGet($id = null)
    {
        if ($id) {
            $this->user->id = $id;
            if ($this->user->readOne()) {
                $user_arr = array(
                    "id" => $this->user->id,
                    "name" => $this->user->name,
                    "email" => $this->user->email,
                    "created_at" => $this->user->created_at,
                    "updated_at" => $this->user->updated_at
                );
                UserView::response(200, $user_arr);
            } else {
                UserView::response(404, array("message" => "Usuario no encontrado."));
            }
        } else {
            $stmt = $this->user->read();
            $num = $stmt->rowCount();

            if ($num > 0) {
                $users_arr = array();
                while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                    extract($row);
                    $user_item = array(
                        "id" => $id,
                        "name" => $name,
                        "email" => $email,
                        "created_at" => $created_at,
                        "updated_at" => $updated_at
                    );
                    array_push($users_arr, $user_item);
                }
                UserView::response(200, $users_arr);
            } else {
                UserView::response(200, array()); // Array vacío si no hay usuarios
            }
        }
    }

    // Manejar peticiones POST
    public function handlePost()
    {
        $data = json_decode(file_get_contents("php://input"));

        if (!empty($data->name) && !empty($data->email) && !empty($data->password)) {
            $this->user->name = $data->name;
            $this->user->email = $data->email;
            $this->user->password = $data->password;

            try {
                if ($this->user->create()) {
                    UserView::response(201, array("message" => "Usuario creado exitosamente."));
                } else {
                    UserView::response(503, array("message" => "No se pudo crear el usuario."));
                }
            } catch (PDOException $e) {
                // Capturar error si el email ya existe (campo UNIQUE)
                UserView::response(400, array("message" => "El correo ya está registrado."));
            }
        } else {
            UserView::response(400, array("message" => "Datos incompletos."));
        }
    }

    // Manejar peticiones PUT
    public function handlePut($id)
    {
        if (!$id) {
            UserView::response(400, array("message" => "ID de usuario requerido."));
        }

        $data = json_decode(file_get_contents("php://input"));

        if (!empty($data->name) && !empty($data->email)) {
            $this->user->id = $id;
            $this->user->name = $data->name;
            $this->user->email = $data->email;
            $this->user->password = !empty($data->password) ? $data->password : null;

            if ($this->user->update()) {
                UserView::response(200, array("message" => "Usuario actualizado correctamente."));
            } else {
                UserView::response(503, array("message" => "No se pudo actualizar el usuario."));
            }
        } else {
            UserView::response(400, array("message" => "Datos incompletos."));
        }
    }

    // Manejar peticiones DELETE
    public function handleDelete($id)
    {
        if (!$id) {
            UserView::response(400, array("message" => "ID de usuario requerido."));
        }

        $this->user->id = $id;

        if ($this->user->delete()) {
            UserView::response(200, array("message" => "Usuario eliminado correctamente."));
        } else {
            UserView::response(503, array("message" => "No se pudo eliminar el usuario."));
        }
    }
}

 

Configuración del enrutamiento: `.htaccess` e `index.php`

Para que nuestra API funcione de forma limpia con URLs amigables (ej. `/api/users/5` en lugar de `/api/index.php?id=5`), necesitamos reescribir las peticiones del servidor mediante un archivo `.htaccess` (si usas Apache/XAMPP) y procesarlas en el archivo centralizado `index.php`.

.htaccess`

Crea el archivo `.htaccess` directamente en la raíz de la carpeta `backend/`:

RewriteEngine On
# Evitar que se acceda a archivos directamente si no existen
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Redirigir todo el tráfico al index.php pasando la ruta como parámetro
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]

index.php

Este será el punto de entrada unificado de nuestro backend. Recibe la URL solicitada, detecta el método HTTP (`GET`, `POST`, etc.) e inicializa el controlador. Escríbelo en `backend/index.php`:

<?php
// Manejo preliminar de peticiones OPTIONS (Preflight en CORS)
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");
    http_response_code(200);
    exit();
}

require_once __DIR__ . '/config/database.php';
require_once __DIR__ . '/controllers/userController.php';

// Inicializar base de datos y obtener conexión
$database = new Database();
$db = $database->getConnection();

// Inicializar el controlador de usuarios
$controller = new UserController($db);

// Analizar la URL amigable
$url = isset($_GET['url']) ? rtrim($_GET['url'], '/') : '';
$urlSegments = explode('/', $url);

// Esperamos rutas del tipo: api/users o api/users/:id
if ($urlSegments[0] === 'api' && isset($urlSegments[1]) && $urlSegments[1] === 'users') {
    
    $id = isset($urlSegments[2]) ? (int)$urlSegments[2] : null;
    $method = $_SERVER['REQUEST_METHOD'];

    switch ($method) {
        case 'GET':
            $controller->handleGet($id);
            break;
        case 'POST':
            $controller->handlePost();
            break;
        case 'PUT':
            $controller->handlePut($id);
            break;
        case 'DELETE':
            $controller->handleDelete($id);
            break;
        default:
            header("HTTP/1.1 405 Method Not Allowed");
            echo json_encode(array("message" => "Método no permitido."));
            break;
    }
} else {
    header("HTTP/1.1 404 Not Found");
    echo json_encode(array("message" => "Endpoint no encontrado."));
}

 

Resumen de lo logrado y Ganchos de la parte 3

¡Felicidades! Con esto hemos completado al 100% el **Backend de nuestra API RESTful**. Si abres Postman o cualquier herramienta de pruebas e intentas hacer un envío `POST` a `http://localhost/tu-proyecto/backend/api/users` enviando un objeto JSON con `name`, `email` y `password`, verás cómo se registra perfectamente en MySQL de forma segura.

Continuará en el siguiente artículo... 💕 En la **tercera y última parte**, pasaremos al lado del cliente: instalaremos **Axios** en nuestro proyecto de **Vue.js 3**, crearemos la interfaz gráfica reactiva con componentes, y conectaremos los formularios para listar, crear, editar y borrar los usuarios en tiempo real.

¡Prepara tus componentes que el frontend nos espera! Dejanos tus dudas en la caja de comentarios si tienes problemas configurando tu servidor local.

Publicaciones relacionadas

Como crear una Plantilla Básica con Materialize

Como crear una Plantilla Básica con Materialize

Materialize es un marco de diseño moderno que utiliza principios de diseño de material para crear interfaces de usuario interactivas y atractivas. Aquí te mostramos cómo puedes crear una plantilla básica utilizando Materialize.

Consultas MySQLi que te pueden ser  muy útiles

Consultas MySQLi que te pueden ser muy útiles

Una serie de consultas MySQLi que puedes utilizar a la hora de comenzar a crear tus propios proyectos utilizando PHP Y MySQL..

Eduardo Nava
Eduardo Nava

Hola mi nombre es Eduardo Nava, Soy programador, diseñador web, móvil y de videojuegos.

El mundo digital avanza cada día.

Soy completamente autodidacta, por eso te ofrezco muchos de mis conocimientos para puedas aprender más fácilmente y más rápido de lo que  hice y hago cada día.

Mi propósito es tratar mi pasión para enseñar tecnologías que avanzan cada día a todas esas personas, que quieran iniciarse en este mundo de forma actualizada.