API REST con PHP desde cero: GET, POST, PUT, DELETE, JSON y PDO


En esta guía vamos a construir una API REST con PHP desde cero utilizando JSON, los métodos HTTP GET, POST, PUT y DELETE, códigos de estado y PDO para trabajar con MySQL.

La idea es entender cómo funciona una API por dentro antes de pasar a frameworks como Laravel. Al finalizar vas a tener una API capaz de listar, consultar, crear, modificar y eliminar registros desde clientes como Postman, JavaScript, aplicaciones móviles u otros sistemas.

¿Qué vamos a construir?

  • una API que devuelva respuestas JSON;
  • endpoint GET para listar usuarios;
  • endpoint GET para consultar un usuario por ID;
  • endpoint POST para crear usuarios;
  • endpoint PUT para modificarlos;
  • endpoint DELETE para eliminarlos;
  • validación de datos;
  • consultas preparadas con PDO;
  • códigos HTTP correctos;
  • manejo básico de errores;
  • pruebas desde Postman.

Antes de empezar

Conviene haber visto previamente estas guías:

1. ¿Qué es una API REST?

Una API permite que dos aplicaciones intercambien información. En una API REST los recursos se identifican mediante URLs y las acciones se expresan principalmente a través de métodos HTTP.

MétodoUso habitualEjemplo
GETConsultar datos/api/usuarios
POSTCrear un recurso/api/usuarios
PUTActualizar un recurso/api/usuarios/5
DELETEEliminar un recurso/api/usuarios/5

Una API no debería depender de páginas HTML. En este caso recibirá y devolverá información en formato JSON.

2. Estructura del proyecto

api-php/
├── config/
│   └── database.php
├── src/
│   ├── UsuarioRepository.php
│   └── response.php
└── public/
    └── index.php

Para mantener el ejemplo claro vamos a utilizar un único punto de entrada público y separar la conexión y el acceso a datos.

3. Crear la base de datos

CREATE DATABASE api_php
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

USE api_php;

CREATE TABLE usuarios (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    activo TINYINT(1) NOT NULL DEFAULT 1,
    creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

4. Conexión con PDO

Archivo config/database.php:

<?php

return new PDO(
    'mysql:host=localhost;dbname=api_php;charset=utf8mb4',
    'root',
    '',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]
);

5. Responder siempre en JSON

Podemos crear una función común para todas las respuestas.

<?php

function responder(
    array $datos,
    int $status = 200
): never {
    http_response_code($status);

    header('Content-Type: application/json; charset=utf-8');

    echo json_encode(
        $datos,
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
    );

    exit;
}

Así evitamos repetir encabezados, código de estado y json_encode() en cada endpoint.

6. Leer el método HTTP y la URL

$metodo = $_SERVER['REQUEST_METHOD'];

$ruta = parse_url(
    $_SERVER['REQUEST_URI'],
    PHP_URL_PATH
);

$segmentos = array_values(
    array_filter(explode('/', $ruta))
);

Para una URL como /api/usuarios/5, los segmentos serán aproximadamente:

[
    'api',
    'usuarios',
    '5'
]

7. Repositorio de usuarios

<?php

class UsuarioRepository
{
    public function __construct(
        private PDO $pdo
    ) {
    }

    public function todos(): array
    {
        $stmt = $this->pdo->query(
            'SELECT id, nombre, email, activo, creado_en
             FROM usuarios
             ORDER BY id DESC'
        );

        return $stmt->fetchAll();
    }

    public function buscar(int $id): ?array
    {
        $stmt = $this->pdo->prepare(
            'SELECT id, nombre, email, activo, creado_en
             FROM usuarios
             WHERE id = :id'
        );

        $stmt->execute(['id' => $id]);

        $usuario = $stmt->fetch();

        return $usuario ?: null;
    }
}

8. GET: listar usuarios

Para GET /api/usuarios:

if (
    $metodo === 'GET'
    && count($segmentos) === 2
) {
    $usuarios = $repository->todos();

    responder([
        'success' => true,
        'data' => $usuarios
    ]);
}

Una respuesta posible sería:

{
    "success": true,
    "data": [
        {
            "id": 1,
            "nombre": "Ana",
            "email": "ana@ejemplo.com"
        }
    ]
}

9. GET: consultar un usuario por ID

if (
    $metodo === 'GET'
    && count($segmentos) === 3
) {
    $id = filter_var(
        $segmentos[2],
        FILTER_VALIDATE_INT
    );

    if (!$id) {
        responder([
            'success' => false,
            'message' => 'ID inválido'
        ], 400);
    }

    $usuario = $repository->buscar($id);

    if (!$usuario) {
        responder([
            'success' => false,
            'message' => 'Usuario no encontrado'
        ], 404);
    }

    responder([
        'success' => true,
        'data' => $usuario
    ]);
}

10. POST: crear un usuario

Cuando enviamos JSON, PHP no lo coloca automáticamente en $_POST. Debemos leer el cuerpo crudo de la petición.

$datos = json_decode(
    file_get_contents('php://input'),
    true
);

if (!is_array($datos)) {
    responder([
        'success' => false,
        'message' => 'JSON inválido'
    ], 400);
}

Después validamos los campos:

$nombre = trim($datos['nombre'] ?? '');
$email = trim($datos['email'] ?? '');

if ($nombre === '') {
    responder([
        'success' => false,
        'message' => 'El nombre es obligatorio'
    ], 422);
}

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    responder([
        'success' => false,
        'message' => 'El email no es válido'
    ], 422);
}

La inserción se realiza mediante una consulta preparada:

$stmt = $pdo->prepare(
    'INSERT INTO usuarios (nombre, email)
     VALUES (:nombre, :email)'
);

$stmt->execute([
    'nombre' => $nombre,
    'email' => $email
]);

$id = (int) $pdo->lastInsertId();

responder([
    'success' => true,
    'message' => 'Usuario creado',
    'id' => $id
], 201);

11. PUT: actualizar un usuario

$stmt = $pdo->prepare(
    'UPDATE usuarios
     SET nombre = :nombre,
         email = :email,
         activo = :activo
     WHERE id = :id'
);

$stmt->execute([
    'nombre' => $nombre,
    'email' => $email,
    'activo' => $activo,
    'id' => $id
]);

responder([
    'success' => true,
    'message' => 'Usuario actualizado'
]);

Antes de ejecutar el UPDATE conviene verificar que el recurso exista. De lo contrario una petición podría responder “actualizado” aunque el ID nunca haya existido.

12. DELETE: eliminar un usuario

$stmt = $pdo->prepare(
    'DELETE FROM usuarios WHERE id = :id'
);

$stmt->execute([
    'id' => $id
]);

responder([
    'success' => true,
    'message' => 'Usuario eliminado'
]);

En sistemas reales puede convenir una baja lógica utilizando un campo como activo o deleted_at, especialmente cuando se necesita conservar historial.

13. Códigos HTTP que conviene usar

CódigoSignificado
200Petición correcta
201Recurso creado
204Respuesta correcta sin contenido
400Petición inválida
401No autenticado
403No autorizado
404Recurso no encontrado
409Conflicto, por ejemplo email duplicado
422Datos válidos como JSON pero incorrectos para la operación
500Error interno del servidor

14. Manejar errores de base de datos

try {
    // ejecutar operación
} catch (PDOException $e) {
    responder([
        'success' => false,
        'message' => 'Error interno del servidor'
    ], 500);
}

En producción no conviene devolver al cliente el mensaje completo de la excepción porque podría revelar nombres de tablas, columnas o detalles de infraestructura. Ese detalle debería registrarse en logs.

15. Router completo simplificado

if ($segmentos[0] ?? null !== 'api') {
    responder([
        'success' => false,
        'message' => 'Ruta no encontrada'
    ], 404);
}

if (($segmentos[1] ?? null) !== 'usuarios') {
    responder([
        'success' => false,
        'message' => 'Recurso no encontrado'
    ], 404);
}

$id = isset($segmentos[2])
    ? (int) $segmentos[2]
    : null;

switch ($metodo) {
    case 'GET':
        // listar o buscar por ID
        break;

    case 'POST':
        // crear
        break;

    case 'PUT':
        // actualizar
        break;

    case 'DELETE':
        // eliminar
        break;

    default:
        responder([
            'success' => false,
            'message' => 'Método no permitido'
        ], 405);
}

16. Probar la API con Postman

Supongamos que el proyecto corre en http://localhost/api-php/public.

  • GET /api/usuarios → listar.
  • GET /api/usuarios/1 → consultar un usuario.
  • POST /api/usuarios → crear.
  • PUT /api/usuarios/1 → actualizar.
  • DELETE /api/usuarios/1 → eliminar.

Para POST y PUT seleccioná Body → raw → JSON y enviá algo como:

{
    "nombre": "Juan Pérez",
    "email": "juan@ejemplo.com"
}

17. ¿Qué pasa con CORS?

Si un frontend JavaScript se ejecuta desde otro origen, el navegador puede bloquear la petición por la política de mismo origen. Para desarrollo podríamos permitir un origen concreto:

header(
    'Access-Control-Allow-Origin: https://mi-frontend.com'
);

header(
    'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'
);

header(
    'Access-Control-Allow-Headers: Content-Type, Authorization'
);

No conviene usar * indiscriminadamente en producción cuando la API maneja credenciales o información sensible.

18. Buenas prácticas básicas

  • usar HTTPS en producción;
  • validar todos los datos recibidos;
  • usar consultas preparadas;
  • no devolver excepciones completas al cliente;
  • usar códigos HTTP coherentes;
  • mantener una estructura JSON consistente;
  • separar acceso a datos de la lógica HTTP;
  • registrar errores en logs;
  • versionar la API cuando empiece a crecer, por ejemplo /api/v1/usuarios;
  • agregar autenticación antes de exponer operaciones sensibles.

19. ¿Cómo quedaría una respuesta consistente?

Éxito:

{
    "success": true,
    "data": {
        "id": 1,
        "nombre": "Juan Pérez"
    }
}

Error:

{
    "success": false,
    "message": "Usuario no encontrado"
}

20. Siguiente paso: autenticación

Esta API todavía es pública. Cualquier cliente que conozca la URL podría intentar crear, modificar o eliminar datos. El siguiente paso natural es implementar autenticación por token y proteger determinados endpoints.

Eso nos permitirá trabajar conceptos como:

  • login mediante API;
  • generación de tokens;
  • header Authorization;
  • endpoints públicos y privados;
  • expiración o revocación de tokens;
  • permisos por usuario o rol.

¿Qué aprendimos?

Construimos la base de una API REST con PHP puro y MySQL. Vimos cómo interpretar métodos HTTP, leer JSON, devolver respuestas JSON, utilizar códigos de estado, validar datos y ejecutar operaciones CRUD mediante PDO.

Entender este flujo antes de utilizar un framework permite comprender mejor qué hacen Laravel, Symfony u otras herramientas cuando construimos APIs más grandes.

Seguí aprendiendo PHP

Podés recorrer toda la ruta desde:

Aprender PHP desde cero: ruta completa →

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando guías, ejercicios y proyectos gratuitos, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Descubre más desde Club Programador

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Un comentario en “API REST con PHP desde cero: GET, POST, PUT, DELETE, JSON y PDO”

Deja una respuesta

Descubre más desde Club Programador

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo