Cómo construir una API RESTful eficiente y segura en PHP 8 con Slim, Eloquent y JWT
En esta guía práctica crearás una API RESTful mínima y profesional usando PHP 8, Slim Framework v4, Eloquent (ORM de Laravel) y JSON Web Tokens (JWT) para autenticación. Te mostraré la estructura de carpetas, dependencias, código completo y las decisiones por qué se toman. Al final tendrás una base lista para producción (con margen de mejora).
Por qué esta stack
- PHP 8: mejoras en rendimiento, tipos y JIT opcional.
- Slim: microframework ligero, middleware-friendly y fácil de usar.
- Eloquent: ORM robusto y productivo incluso fuera de Laravel.
- JWT: estándar para tokens stateless, fácil integración con SPAs o móviles.
Estructura de carpetas
project-root/
├─ public/
│ └─ index.php # Front controller
├─ src/
│ ├─ Controllers/
│ │ └─ AuthController.php
│ ├─ Middleware/
│ │ └─ JwtMiddleware.php
│ ├─ Models/
│ │ └─ User.php
│ ├─ config.php
│ └─ dependencies.php
├─ migrations/
├─ .env
├─ composer.json
└─ README.md
Instalación rápida
composer require slim/slim:^4 slim/psr7 nyholm/psr7
composer require illuminate/database:^8 vlucas/phpdotenv firebase/php-jwt
composer require respect/validation
Usamos nyholm/psr7 para PSR-7, illuminate/database para Eloquent, phpdotenv para configuración y firebase/php-jwt para tokens.
Archivo de configuración: .env
APP_ENV=development
APP_DEBUG=true
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=api_db
DB_USERNAME=root
DB_PASSWORD=secret
JWT_SECRET=tu_secreto_muy_largo_y_seguro
Bootstrap: public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
use Slim\Factory\AppFactory;
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->safeLoad();
$app = AppFactory::create();
// Carga dependencias y config
(require __DIR__ . '/../src/dependencies.php')($app);
(require __DIR__ . '/../src/config.php')($app);
// Rutas
(require __DIR__ . '/../src/routes.php')($app);
$app->run();
Dependencias y Eloquent: src/dependencies.php
<?php
use Psr\Container\ContainerInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
return function($app) {
// Configuración Eloquent
$capsule = new Capsule;
$capsule->addConnection([
'driver' => getenv('DB_DRIVER'),
'host' => getenv('DB_HOST'),
'database' => getenv('DB_DATABASE'),
'username' => getenv('DB_USERNAME'),
'password' => getenv('DB_PASSWORD'),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
// Registrar en el container si lo necesitas
$container = $app->getContainer();
if ($container) {
$container->set('db', function() use ($capsule) {
return $capsule;
});
}
};
Configuración adicional: src/config.php
<?php
return function($app) {
// Error middleware básico
$displayErrorDetails = getenv('APP_DEBUG') === 'true';
$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, true, true);
};
Modelo User: src/Models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'users';
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password'];
public $timestamps = true;
}
Por qué: ocultamos el password en serializaciones y permitimos fillable para asignación masiva controlada.
Middleware JWT: src/Middleware/JwtMiddleware.php
<?php
namespace App\Middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtMiddleware
{
public function __invoke(Request $request, Response $response, $next)
{
$authHeader = $request->getHeaderLine('Authorization');
if (!$authHeader || !preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
$response->getBody()->write(json_encode(['error' => 'Token not provided']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
}
$token = $matches[1];
try {
$secret = getenv('JWT_SECRET');
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
// attach user data to request (puedes consultar DB aquí)
$request = $request->withAttribute('jwt', $decoded);
return $next($request, $response);
} catch (\Exception $e) {
$response->getBody()->write(json_encode(['error' => 'Invalid token', 'message' => $e->getMessage()]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
}
}
}
Por qué: el middleware verifica y descodifica el JWT y añade la información al request para los controladores. Usar Key con firebase/php-jwt para compatibilidad con HS256 y futuros algoritmos.
Controlador de autenticación: src/Controllers/AuthController.php
<?php
namespace App\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use App\Models\User;
use Firebase\JWT\JWT;
use Respect\Validation\Validator as v;
class AuthController
{
public function login(Request $request, Response $response)
{
$data = (array)$request->getParsedBody();
$email = $data['email'] ?? '';
$password = $data['password'] ?? '';
// Validación básica
$emailValidator = v::email()->notEmpty();
$passwordValidator = v::stringType()->notEmpty();
if (!$emailValidator->validate($email) || !$passwordValidator->validate($password)) {
$response->getBody()->write(json_encode(['error' => 'Invalid credentials']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$user = User::where('email', $email)->first();
if (!$user || !password_verify($password, $user->password)) {
$response->getBody()->write(json_encode(['error' => 'Invalid email or password']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
}
$payload = [
'sub' => $user->id,
'email' => $user->email,
'iat' => time(),
'exp' => time() + 3600
];
$jwt = JWT::encode($payload, getenv('JWT_SECRET'), 'HS256');
$response->getBody()->write(json_encode(['token' => $jwt]));
return $response->withHeader('Content-Type', 'application/json');
}
public function me(Request $request, Response $response)
{
$jwt = $request->getAttribute('jwt');
$user = User::find($jwt->sub);
$response->getBody()->write(json_encode(['user' => $user]));
return $response->withHeader('Content-Type', 'application/json');
}
}
Rutas: src/routes.php
<?php
use App\Controllers\AuthController;
use App\Middleware\JwtMiddleware;
return function($app) {
$app->post('/login', [AuthController::class, 'login']);
$app->get('/me', function($request, $response) {
// route-level closure ejemplo
});
// Rutas protegidas
$app->group('/api', function($group) {
$group->get('/me', [AuthController::class, 'me']);
// Más rutas CRUD
})->add(new JwtMiddleware());
};
Migraciones y seguridad de contraseñas
Usa migrations para crear la tabla users. Guarda contraseñas con password_hash(..., PASSWORD_DEFAULT). Nunca guardes JWT secrets en el repositorio. Rotación periódica de claves y uso de KMS (AWS KMS, HashiCorp Vault) en producción es recomendable.
Buenas prácticas y performance
- Usa middleware para autenticación, rate limiting (ej. symfony/rate-limiter o middleware personalizado) y logging.
- Serializa respuestas consistentemente: usa un response factory que envuelva la respuesta en { data: ..., error: ... }.
- Evita consultas N+1 con Eloquent: usa with() para relaciones.
- Cachea respuestas públicas con Redis o Varnish para endpoints intensivos en lectura.
- Activa OPcache y, si aplicable, RoadRunner o Swoole para mejorar latencia y throughput.
Ejemplo de petición
POST /login
Content-Type: application/json
{"email":"user@example.com","password":"secret"}
Respuesta 200:
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."}
Uso en rutas protegidas:
Authorization: Bearer <token>
Errores comunes y cómo evitarlos
- No validar entrada: usa Respect/Validation o DTOs con argumentos tipados.
- Exponer toda la excepción en producción: habilita displayErrorDetails solo en dev.
- Tokens demasiado largos sin expiración: establece exp corta y refresh tokens si hace falta.
- Conexiones DB sin pool en entornos serverless: usa drivers y estrategias adecuados.
Por qué evitar: estas fallas llevan a fugas de datos, mala experiencia y problemas de escalabilidad.
Pruebas y validación
- Escribe tests para rutas con phpunit y requests simuladas.
- Prueba la expiración y revocación de tokens.
- Fuzzing de entrada para endpoints críticos.
Implementa monitors (Sentry) y métricas (Prometheus) para detectar regresiones y latencias.
Si quieres aplicar esto en producción: agrega HTTPS obligatorio, CSP, CORS configurado solo para orígenes permitidos y auditing de dependencias (composer audit / SensioLabs Security Checker si disponible).
Próximo paso avanzado: considera dividir la capa de autenticación en un microservicio separado y usar OAuth2 / OpenID Connect (Keycloak, Authelia) si necesitas SSO, refresh tokens y scopes granulares.
Advertencia: nunca uses JWT sin expiración o sin un mecanismo de revocación si tu aplicación requiere invalidar tokens inmediatamente (logout forzado, cambio de permisos). Implementa short-lived access tokens + refresh tokens seguros con revocación server-side.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación