Cómo construir una API RESTful en PHP con Slim, PDO y JWT

php Cómo construir una API RESTful en PHP con Slim, PDO y JWT

Cómo construir una API RESTful en PHP con Slim, PDO y JWT

Guía práctica para montar una API segura y ligera usando Slim Framework, PDO para acceso a la base de datos y JSON Web Tokens para autenticación. Incluye estructura de carpetas, código completo mínimo y explicación del por qué de cada decisión.

Requisitos

  • PHP 8.0+
  • Composer
  • MySQL o similar
  • Conocimientos básicos de HTTP y JSON

Estructura de proyecto

project/
├─ public/
│  └─ index.php
├─ src/
│  ├─ controllers/
│  │  ├─ AuthController.php
│  │  └─ UserController.php
│  ├─ middleware/
│  │  └─ JwtMiddleware.php
│  ├─ models/
│  │  └─ User.php
│  ├─ routes.php
│  └─ dependencies.php
├─ config/
│  └─ settings.php
├─ vendor/
├─ composer.json
└─ migrations/
   └─ create_users.sql

Dependencias (composer)

{
  'require': {
    'slim/slim': '^4.10',
    'slim/psr7': '^1.6',
    'firebase/php-jwt': '^6.4',
    'vlucas/phpdotenv': '^5.5'
  }
}

Base de datos (SQL)

-- migrations/create_users.sql
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  name VARCHAR(100),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Configuración

// config/settings.php
return [
  'db' => [
    'dsn' => 'mysql:host=127.0.0.1;dbname=myapi;charset=utf8mb4',
    'user' => 'dbuser',
    'pass' => 'dbpass'
  ],
  'jwt' => [
    'secret' => 'cambia_esta_clave_super_secreta',
    'expires_in' => 3600
  ]
];

Punto de entrada

// public/index.php
run();

Dependencias y contenedor

// src/dependencies.php
getContainer();

  $container->set('db', function() use ($settings) {
    $db = $settings['db'];
    $pdo = new PDO($db['dsn'], $db['user'], $db['pass'], [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]);
    return $pdo;
  });

  $container->set('settings', function() use ($settings) {
    return $settings;
  });
};

Rutas

// src/routes.php
post('/login', '\\App\\Controllers\\AuthController:login');
  $app->post('/register', '\\App\\Controllers\\AuthController:register');

  // Protected group
  $app->group('/api', function($group) {
    $group->get('/me', '\\App\\Controllers\\UserController:me');
  })->add(new \\App\\Middleware\\JwtMiddleware($settings['jwt']['secret']));
};

Controlador de autenticación

// src/controllers/AuthController.php
container = $c; }

  public function register(Request $req, Response $res) {
    $data = (array)$req->getParsedBody();
    if (empty($data['email']) || empty($data['password'])) {
      return $res->withStatus(400)->withHeader('Content-Type', 'application/json')
                 ->write(json_encode(['error' => 'email y password requeridos']));
    }

    $pdo = $this->container->get('db');
    $hash = password_hash($data['password'], PASSWORD_DEFAULT);
    $stmt = $pdo->prepare('INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)');
    $stmt->execute([$data['email'], $hash, $data['name'] ?? null]);

    return $res->withHeader('Content-Type', 'application/json')
               ->write(json_encode(['ok' => true]));
  }

  public function login(Request $req, Response $res) {
    $data = (array)$req->getParsedBody();
    $pdo = $this->container->get('db');
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
    $stmt->execute([$data['email']]);
    $user = $stmt->fetch();
    if (!$user || !password_verify($data['password'], $user['password_hash'])) {
      return $res->withStatus(401)->withHeader('Content-Type', 'application/json')
                 ->write(json_encode(['error' => 'credenciales inválidas']));
    }

    $settings = $this->container->get('settings');
    $now = time();
    $exp = $now + $settings['jwt']['expires_in'];
    $payload = [
      'sub' => $user['id'],
      'iat' => $now,
      'exp' => $exp
    ];
    $token = JWT::encode($payload, $settings['jwt']['secret'], 'HS256');

    return $res->withHeader('Content-Type', 'application/json')
               ->write(json_encode(['token' => $token]));
  }
}

Middleware JWT

// src/middleware/JwtMiddleware.php
secret = $secret; }

  public function __invoke($request, $handler) {
    $header = $request->getHeaderLine('Authorization');
    if (!$header || strpos($header, 'Bearer ') !== 0) {
      return (new \Slim\Psr7\Response())->withStatus(401)->withHeader('Content-Type', 'application/json')
        ->write(json_encode(['error' => 'token requerido']));
    }
    $token = substr($header, 7);

    try {
      $decoded = JWT::decode($token, new Key($this->secret, 'HS256'));
    } catch (\Exception $e) {
      return (new \Slim\Psr7\Response())->withStatus(401)->withHeader('Content-Type', 'application/json')
        ->write(json_encode(['error' => 'token inválido']));
    }

    // Adjunta el user id al request para downstream
    $request = $request->withAttribute('user_id', $decoded->sub);
    return $handler->handle($request);
  }
}

Controlador de usuario

// src/controllers/UserController.php
container = $c; }

  public function me(Request $req, Response $res) {
    $id = $req->getAttribute('user_id');
    $pdo = $this->container->get('db');
    $stmt = $pdo->prepare('SELECT id, email, name, created_at FROM users WHERE id = ?');
    $stmt->execute([$id]);
    $user = $stmt->fetch();
    return $res->withHeader('Content-Type', 'application/json')
               ->write(json_encode($user ?: []));
  }
}

Por qué estas decisiones

  • Slim: micro-framework muy ligero para APIs, alta compatibilidad PSR-7/15.
  • PDO: consultas preparadas y control de errores; evita inyección SQL cuando se usa correctamente.
  • JWT: buen trade-off para APIs stateless; evita lecturas de sesión en cada petición.
  • password_hash/password_verify: hashing seguro sin implementar tu propia lógica.

Pruebas rápidas

  • Registrar: POST /register {"email":"a@b.com","password":"1234"}
  • Login: POST /login {"email":"a@b.com","password":"1234"} -> recibe token
  • Obtener perfil: GET /api/me con Header Authorization: Bearer <token>

Puntos críticos de seguridad

  • Cambia la clave JWT por algo con suficiente entropía y guárdala en entorno (no en código).
  • Considera rotación de claves y lista de revocación para tokens comprometidos.
  • Limita el tiempo de vida del token y usa refresh tokens con almacenamiento seguro si necesitas sesiones más largas.
  • Valida y sanitiza entradas; usa CORS y rate limiting en producción.

Siguiente paso: empaca la app en Docker, añade tests automatizados (PHPUnit), un endpoint de refresh seguro y considera usar una capa de caching y rate limiter (Redis) para producción. Ten cuidado con la revocación de JWTs: sin un mecanismo de revocación los tokens permanecen válidos hasta expirar.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión