Criando uma API REST sem framework
Muitos desenvolvedores PHP recorrem automaticamente a frameworks como Laravel ou Symfony para construir APIs REST. No entanto, compreender como criar uma API do zero é fundamental para dominar os conceitos subjacentes e ter controle total sobre o comportamento da aplicação. Este artigo demonstra passo a passo como construir uma API REST funcional utilizando apenas PHP puro, sem dependências externas.
1. Fundamentos da Arquitetura REST em PHP puro
REST (Representational State Transfer) é um estilo arquitetural baseado em recursos identificados por URLs, operados através de verbos HTTP padronizados (GET, POST, PUT, DELETE, PATCH) e que deve ser stateless — cada requisição contém toda informação necessária para ser processada.
A estrutura mínima de uma API REST em PHP puro consiste em:
- Um ponto de entrada único (front controller):
index.php - Roteamento manual baseado em
$_SERVER['REQUEST_URI'] - Respostas formatadas como JSON
Para preparar o ambiente, utilize o servidor embutido do PHP:
php -S localhost:8000 index.php
Este comando redireciona todas as requisições para o index.php, que atuará como front controller.
2. Implementando o Roteador (Router) do zero
O roteador é o coração da API. Ele interpreta a URI e o método HTTP, direcionando a requisição para o controlador adequado. Veja uma implementação básica:
<?php
// index.php - Front Controller
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Remove trailing slash
$uri = rtrim($uri, '/');
// Definição de rotas
$routes = [
'GET' => [
'/api/users' => 'UserController@index',
'/api/users/{id}' => 'UserController@show',
],
'POST' => [
'/api/users' => 'UserController@store',
],
'PUT' => [
'/api/users/{id}' => 'UserController@update',
],
'DELETE' => [
'/api/users/{id}' => 'UserController@destroy',
],
];
// Função de roteamento
function route($method, $uri, $routes) {
if (!isset($routes[$method])) {
http_response_code(405);
echo json_encode(['error' => 'Method Not Allowed']);
return;
}
foreach ($routes[$method] as $pattern => $handler) {
// Converte padrão {id} para expressão regular
$regex = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern);
$regex = '#^' . $regex . '$#';
if (preg_match($regex, $uri, $matches)) {
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
return ['handler' => $handler, 'params' => $params];
}
}
http_response_code(404);
echo json_encode(['error' => 'Not Found']);
return null;
}
$route = route($method, $uri, $routes);
3. Controladores e Respostas Estruturadas
Os controladores organizam a lógica de negócio por recurso. Implementamos funções auxiliares para padronizar respostas JSON:
<?php
// helpers.php
function jsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
function jsonError($message, $statusCode = 400) {
jsonResponse(['error' => $message], $statusCode);
}
// UserController.php
class UserController {
public function index() {
$users = getAllUsers(); // Função que busca do banco
jsonResponse($users);
}
public function show($id) {
$user = getUserById($id);
if (!$user) {
jsonError('User not found', 404);
}
jsonResponse($user);
}
public function store() {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['name'], $data['email'])) {
jsonError('Name and email are required', 400);
}
$id = createUser($data);
jsonResponse(['id' => $id, 'message' => 'User created'], 201);
}
public function update($id) {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
jsonError('Invalid JSON input', 400);
}
$updated = updateUser($id, $data);
if (!$updated) {
jsonError('User not found', 404);
}
jsonResponse(['message' => 'User updated']);
}
public function destroy($id) {
$deleted = deleteUser($id);
if (!$deleted) {
jsonError('User not found', 404);
}
jsonResponse(null, 204);
}
}
4. Manipulação de Dados: Entrada e Validação
A captura de dados varia conforme o método HTTP:
$_GETpara parâmetros de query string$_POSTpara formulários URL-encodedphp://inputpara requisições com corpo JSON
Implemente validação manual robusta:
<?php
function validateUserInput($data) {
$errors = [];
// Campo obrigatório
if (empty($data['name'])) {
$errors[] = 'Name is required';
}
// Validação de email
if (!filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Valid email is required';
}
// Validação de tipo numérico
if (isset($data['age']) && !ctype_digit((string)$data['age'])) {
$errors[] = 'Age must be an integer';
}
// Sanitização contra XSS
$data['name'] = htmlspecialchars(strip_tags($data['name']), ENT_QUOTES, 'UTF-8');
return ['data' => $data, 'errors' => $errors];
}
5. Integração com Banco de Dados (PDO)
PDO com prepared statements é a forma segura de interagir com bancos de dados:
<?php
// Database.php
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
$dsn = 'mysql:host=localhost;dbname=api_db;charset=utf8mb4';
$this->pdo = new PDO($dsn, 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
public static function getInstance() {
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function getPdo() {
return $this->pdo;
}
}
// Funções CRUD genéricas
function getAllUsers() {
$pdo = Database::getInstance()->getPdo();
$stmt = $pdo->query('SELECT id, name, email, created_at FROM users');
return $stmt->fetchAll();
}
function getUserById($id) {
$pdo = Database::getInstance()->getPdo();
$stmt = $pdo->prepare('SELECT id, name, email, created_at FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->fetch();
}
function createUser($data) {
$pdo = Database::getInstance()->getPdo();
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
'name' => $data['name'],
'email' => $data['email'],
'age' => $data['age'] ?? null,
]);
return $pdo->lastInsertId();
}
function updateUser($id, $data) {
$pdo = Database::getInstance()->getPdo();
$fields = [];
$params = ['id' => $id];
foreach (['name', 'email', 'age'] as $field) {
if (isset($data[$field])) {
$fields[] = "$field = :$field";
$params[$field] = $data[$field];
}
}
if (empty($fields)) {
return false;
}
$sql = 'UPDATE users SET ' . implode(', ', $fields) . ' WHERE id = :id';
$stmt = $pdo->prepare($sql);
return $stmt->execute($params);
}
function deleteUser($id) {
$pdo = Database::getInstance()->getPdo();
$stmt = $pdo->prepare('DELETE FROM users WHERE id = :id');
return $stmt->execute(['id' => $id]);
}
6. Tratamento de Erros e Exceções
Configurar handlers globais garante que erros inesperados retornem JSON:
<?php
// error_handler.php
// Exceções personalizadas
class ValidationException extends Exception {}
class NotFoundException extends Exception {}
// Handler de exceções
set_exception_handler(function ($exception) {
$statusCode = 500;
$message = 'Internal Server Error';
if ($exception instanceof ValidationException) {
$statusCode = 400;
$message = $exception->getMessage();
} elseif ($exception instanceof NotFoundException) {
$statusCode = 404;
$message = $exception->getMessage();
}
// Log do erro interno (não exposto ao cliente)
error_log($exception->getMessage() . ' in ' . $exception->getFile() . ':' . $exception->getLine());
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode(['error' => $message]);
exit;
});
// Handler de erros PHP (converte em exceções)
set_error_handler(function ($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
7. Segurança Básica e Boas Práticas
Adicione headers de segurança e implemente rate limiting simples:
<?php
// security.php
// Headers de segurança
header('Content-Security-Policy: default-src \'self\'');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
// CORS (ajuste para produção)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Rate Limiting simples
function checkRateLimit($ip, $maxRequests = 100, $timeWindow = 3600) {
$file = sys_get_temp_dir() . "/rate_limit_$ip.json";
$now = time();
$data = [];
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true) ?: [];
// Remove requisições antigas
$data = array_filter($data, fn($time) => $time > ($now - $timeWindow));
}
if (count($data) >= $maxRequests) {
http_response_code(429);
echo json_encode(['error' => 'Too Many Requests']);
exit;
}
$data[] = $now;
file_put_contents($file, json_encode($data));
}
// Aplicar rate limiting
checkRateLimit($_SERVER['REMOTE_ADDR']);
// Versionamento via prefixo na URL
// As rotas já consideram /api/ como prefixo
// Para v2, crie rotas com /api/v2/
Conclusão
Criar uma API REST sem framework em PHP é um exercício valioso que demonstra profundo entendimento dos fundamentos web. Você ganha controle total sobre cada aspecto da aplicação, desde roteamento até segurança, sem depender de abstrações de terceiros. Para projetos reais, considere a complexidade versus benefícios — frameworks oferecem conveniência, mas o conhecimento adquirido aqui é transferível para qualquer stack.
Referências
- PHP: The Right Way - RESTful APIs — Guia oficial da comunidade PHP sobre boas práticas para APIs RESTful
- PHP Manual: PDO — Documentação oficial do PDO, incluindo prepared statements e conexão segura com bancos de dados
- MDN Web Docs: HTTP response status codes — Referência completa dos códigos de status HTTP e seus significados
- OWASP: Input Validation Cheat Sheet — Guia de validação e sanitização de entrada para segurança de aplicações web
- RESTful API Design: Best Practices — Artigo abrangente sobre design de APIs REST, incluindo versionamento e nomenclatura de recursos
- PHP: Built-in web server — Documentação oficial do servidor embutido do PHP para desenvolvimento local
- Fowler: Richardson Maturity Model — Modelo de maturidade REST de Leonard Richardson, útil para entender níveis de adoção REST