Suas ideias em realidade digital!
>
Tratar erros corretamente em uma API PHP é essencial para criar uma aplicação mais profissional, segura e fácil de consumir. Quando uma API retorna erros desorganizados, mensagens em HTML, tela branca ou códigos HTTP incorretos, o frontend, o aplicativo mobile ou outro sistema integrado pode ter dificuldade para entender o que aconteceu.
Em uma API PHP puro, sem framework, é muito comum o desenvolvedor começar retornando apenas mensagens simples com echo.
Porém, conforme o projeto cresce, fica necessário padronizar as respostas, separar erros de validação, autenticação, permissão,
rota inexistente e falhas internas do servidor.
Neste artigo, você vai aprender como criar um tratamento de erros em API PHP com respostas JSON padronizadas para os principais status HTTP:
400, 401, 403, 404, 422 e 500.
Uma API pode ser consumida por diferentes clientes: um site, um sistema administrativo, um aplicativo Android, uma integração externa ou até outro servidor. Por isso, as respostas precisam seguir um padrão previsível.
Quando cada endpoint retorna um erro de um jeito diferente, o frontend precisa criar vários tratamentos específicos. Isso aumenta a complexidade e dificulta a manutenção.
Veja alguns problemas comuns em APIs sem tratamento padronizado:
200 OK mesmo quando ocorreu erro;Uma boa estrutura de erro melhora a segurança, facilita o consumo da API e deixa o sistema muito mais profissional.
Uma resposta de erro simples e eficiente pode seguir este formato:
{
"success": false,
"message": "Mensagem principal do erro.",
"errors": {
"campo": "Detalhe do erro"
}
}
Para erros mais simples, você pode retornar apenas:
{
"success": false,
"message": "Recurso não encontrado."
}
O importante é manter o mesmo padrão em toda a API.
O primeiro passo é criar uma função reutilizável para retornar respostas JSON com status HTTP.
<?php
function jsonResponse(int $statusCode, array $data): void
{
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
Com essa função, todos os endpoints podem responder da mesma forma.
Exemplo de uso:
<?php
jsonResponse(404, [
'success' => false,
'message' => 'Usuário não encontrado.'
]);
Antes de criar o tratamento, é importante entender quando usar cada código HTTP.
| Status | Nome | Quando usar |
|---|---|---|
200 |
OK | Quando a requisição foi processada com sucesso. |
201 |
Created | Quando um novo registro foi criado com sucesso. |
400 |
Bad Request | Quando a requisição está malformada, como JSON inválido. |
401 |
Unauthorized | Quando o usuário não enviou token ou a autenticação falhou. |
403 |
Forbidden | Quando o usuário está autenticado, mas não tem permissão. |
404 |
Not Found | Quando a rota ou recurso solicitado não existe. |
422 |
Unprocessable Entity | Quando o JSON é válido, mas os dados não passam na validação. |
500 |
Internal Server Error | Quando ocorre uma falha inesperada no servidor. |
O status 400 Bad Request deve ser usado quando a requisição enviada para a API está malformada.
Um exemplo comum é quando o corpo da requisição não contém um JSON válido.
<?php
$input = file_get_contents('php://input');
$dados = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
jsonResponse(400, [
'success' => false,
'message' => 'JSON inválido.',
'errors' => [
'json' => 'O corpo da requisição não contém um JSON válido.'
]
]);
}
Esse tipo de erro acontece antes mesmo da validação dos campos, porque a API não conseguiu interpretar corretamente o corpo da requisição.
O status 401 Unauthorized deve ser usado quando a requisição precisa de autenticação, mas o usuário não enviou um token válido.
Exemplo: uma rota protegida espera o cabeçalho Authorization: Bearer TOKEN, mas ele não foi enviado.
<?php
$headers = getallheaders();
$authorization = $headers['Authorization'] ?? $headers['authorization'] ?? '';
if (empty($authorization)) {
jsonResponse(401, [
'success' => false,
'message' => 'Token de autenticação não informado.'
]);
}
if (!str_starts_with($authorization, 'Bearer ')) {
jsonResponse(401, [
'success' => false,
'message' => 'Formato do token inválido.'
]);
}
401 quando o problema for autenticação. Se o usuário estiver autenticado, mas não tiver permissão,
use 403.
O status 403 Forbidden deve ser usado quando o usuário está autenticado, mas não tem permissão para acessar aquele recurso.
Exemplo: um usuário comum tenta acessar uma rota administrativa.
<?php
$usuario = [
'id' => 10,
'nome' => 'João',
'role' => 'USER'
];
if ($usuario['role'] !== 'ADMIN') {
jsonResponse(403, [
'success' => false,
'message' => 'Você não tem permissão para acessar este recurso.'
]);
}
Esse padrão ajuda o frontend a entender que o usuário está logado, mas não pode executar aquela ação.
O status 404 Not Found pode ser usado em dois cenários principais:
Exemplo de recurso não encontrado:
<?php
$id = $_GET['id'] ?? null;
$usuario = null; // Resultado da busca no banco
if (!$usuario) {
jsonResponse(404, [
'success' => false,
'message' => 'Usuário não encontrado.'
]);
}
Também é interessante configurar seu roteador para retornar 404 quando nenhuma rota for encontrada.
<?php
jsonResponse(404, [
'success' => false,
'message' => 'Rota não encontrada.'
]);
O status 422 Unprocessable Entity é muito útil para erros de validação.
Ele indica que a API entendeu a requisição, mas os dados enviados não passaram nas regras da aplicação.
Exemplo:
<?php
$erros = [];
if (empty($dados['nome'])) {
$erros['nome'] = 'O nome é obrigatório.';
}
if (empty($dados['email'])) {
$erros['email'] = 'O e-mail é obrigatório.';
} elseif (!filter_var($dados['email'], FILTER_VALIDATE_EMAIL)) {
$erros['email'] = 'Informe um e-mail válido.';
}
if (!empty($erros)) {
jsonResponse(422, [
'success' => false,
'message' => 'Existem campos inválidos.',
'errors' => $erros
]);
}
Esse formato é excelente para formulários, pois o frontend consegue exibir cada erro abaixo do campo correspondente.
O status 500 Internal Server Error deve ser usado quando ocorre uma falha inesperada no servidor.
Pode ser um erro no banco, uma exception não tratada, arquivo inexistente, falha de conexão ou problema interno da aplicação.
Em produção, nunca é recomendado retornar a mensagem real do erro para o usuário. O ideal é registrar o erro em log e devolver uma mensagem genérica.
<?php
try {
// Exemplo de operação no banco
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE id = :id");
$stmt->execute([
':id' => $id
]);
$usuario = $stmt->fetch();
} catch (PDOException $e) {
error_log('Erro no banco: ' . $e->getMessage());
jsonResponse(500, [
'success' => false,
'message' => 'Erro interno no servidor.'
]);
}
Para deixar o código ainda mais limpo, você pode criar uma função específica para erros.
<?php
function errorResponse(int $statusCode, string $message, array $errors = []): void
{
$response = [
'success' => false,
'message' => $message
];
if (!empty($errors)) {
$response['errors'] = $errors;
}
jsonResponse($statusCode, $response);
}
Agora você pode usar assim:
<?php
errorResponse(404, 'Usuário não encontrado.');
errorResponse(422, 'Existem campos inválidos.', [
'email' => 'Informe um e-mail válido.',
'senha' => 'A senha deve ter pelo menos 6 caracteres.'
]);
Em projetos maiores, uma boa prática é criar uma exception personalizada para erros da API. Assim, você pode lançar erros com status HTTP e mensagem personalizada.
<?php
class ApiException extends Exception
{
private int $statusCode;
private array $errors;
public function __construct(string $message, int $statusCode = 500, array $errors = [])
{
parent::__construct($message);
$this->statusCode = $statusCode;
$this->errors = $errors;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function getErrors(): array
{
return $this->errors;
}
}
Com essa classe, você consegue centralizar o tratamento de erros.
Agora veja um exemplo completo usando try, catch e a classe ApiException.
<?php
try {
$id = $_GET['id'] ?? null;
if (empty($id)) {
throw new ApiException('ID não informado.', 400, [
'id' => 'Informe o ID do usuário.'
]);
}
if (!is_numeric($id)) {
throw new ApiException('ID inválido.', 422, [
'id' => 'O ID deve ser numérico.'
]);
}
// Exemplo: busca no banco
$usuario = null;
if (!$usuario) {
throw new ApiException('Usuário não encontrado.', 404);
}
jsonResponse(200, [
'success' => true,
'data' => $usuario
]);
} catch (ApiException $e) {
errorResponse($e->getStatusCode(), $e->getMessage(), $e->getErrors());
} catch (Throwable $e) {
error_log('Erro inesperado: ' . $e->getMessage());
errorResponse(500, 'Erro interno no servidor.');
}
Esse modelo é simples, mas já deixa sua API muito mais organizada.
Também é possível configurar manipuladores globais para capturar exceptions não tratadas e erros fatais.
<?php
set_exception_handler(function (Throwable $e) {
error_log('Exception não tratada: ' . $e->getMessage());
jsonResponse(500, [
'success' => false,
'message' => 'Erro interno no servidor.'
]);
});
set_error_handler(function ($severity, $message, $file, $line) {
error_log("Erro PHP: {$message} em {$file}:{$line}");
jsonResponse(500, [
'success' => false,
'message' => 'Erro interno no servidor.'
]);
});
Isso ajuda a evitar que a API retorne uma tela branca ou erro HTML inesperado.
Durante o desenvolvimento, pode ser útil ver detalhes do erro. Porém, em produção, isso deve ficar desativado.
<?php
define('APP_DEBUG', false);
catch (Throwable $e) {
error_log('Erro inesperado: ' . $e->getMessage());
$response = [
'success' => false,
'message' => 'Erro interno no servidor.'
];
if (APP_DEBUG) {
$response['debug'] = [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
];
}
jsonResponse(500, $response);
}
Em produção, mantenha APP_DEBUG como false.
Uma estrutura simples para organizar o tratamento de erros pode ser:
/api
/core
response.php
ApiException.php
error-handler.php
/controllers
UsuarioController.php
/middlewares
AuthMiddleware.php
index.php
No arquivo response.php, você pode deixar:
<?php
function jsonResponse(int $statusCode, array $data): void
{
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function errorResponse(int $statusCode, string $message, array $errors = []): void
{
$response = [
'success' => false,
'message' => $message
];
if (!empty($errors)) {
$response['errors'] = $errors;
}
jsonResponse($statusCode, $response);
}
No arquivo ApiException.php, você deixa a classe personalizada:
<?php
class ApiException extends Exception
{
private int $statusCode;
private array $errors;
public function __construct(string $message, int $statusCode = 500, array $errors = [])
{
parent::__construct($message);
$this->statusCode = $statusCode;
$this->errors = $errors;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function getErrors(): array
{
return $this->errors;
}
}
No arquivo error-handler.php, você centraliza os erros globais:
<?php
set_exception_handler(function (Throwable $e) {
error_log('Exception não tratada: ' . $e->getMessage());
jsonResponse(500, [
'success' => false,
'message' => 'Erro interno no servidor.'
]);
});
set_error_handler(function ($severity, $message, $file, $line) {
error_log("Erro PHP: {$message} em {$file}:{$line}");
jsonResponse(500, [
'success' => false,
'message' => 'Erro interno no servidor.'
]);
});
Abaixo temos um exemplo simples de endpoint para buscar usuário por ID.
<?php
require_once __DIR__ . '/core/response.php';
require_once __DIR__ . '/core/ApiException.php';
require_once __DIR__ . '/core/error-handler.php';
try {
$id = $_GET['id'] ?? null;
if (empty($id)) {
throw new ApiException('ID não informado.', 400, [
'id' => 'Informe o ID do usuário.'
]);
}
if (!is_numeric($id)) {
throw new ApiException('ID inválido.', 422, [
'id' => 'O ID deve ser um número.'
]);
}
$pdo = new PDO(
'mysql:host=localhost;dbname=sua_base;charset=utf8mb4',
'usuario',
'senha',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
$stmt = $pdo->prepare("SELECT id, nome, email FROM usuarios WHERE id = :id");
$stmt->execute([
':id' => $id
]);
$usuario = $stmt->fetch();
if (!$usuario) {
throw new ApiException('Usuário não encontrado.', 404);
}
jsonResponse(200, [
'success' => true,
'data' => $usuario
]);
} catch (ApiException $e) {
errorResponse($e->getStatusCode(), $e->getMessage(), $e->getErrors());
} catch (PDOException $e) {
error_log('Erro PDO: ' . $e->getMessage());
errorResponse(500, 'Erro interno ao consultar os dados.');
} catch (Throwable $e) {
error_log('Erro inesperado: ' . $e->getMessage());
errorResponse(500, 'Erro interno no servidor.');
}
É importante não misturar erro de validação com erro interno.
Um erro de validação acontece quando o usuário enviou algo errado, como e-mail inválido, campo obrigatório vazio ou ID em formato incorreto.
Nesse caso, normalmente usamos 400 ou 422.
Já o erro interno acontece quando o problema está no servidor, banco, código ou infraestrutura.
Nesse caso, usamos 500.
| Tipo de erro | Exemplo | Status recomendado |
|---|---|---|
| JSON inválido | Corpo da requisição quebrado | 400 |
| Campo inválido | E-mail fora do formato | 422 |
| Sem autenticação | Token não enviado | 401 |
| Sem permissão | Usuário comum acessando rota admin | 403 |
| Não encontrado | ID inexistente | 404 |
| Erro interno | Falha no banco de dados | 500 |
Um erro comum é retornar success: false, mas manter o status HTTP como 200 OK.
Isso confunde o cliente que consome a API.
Evite:
{
"success": false,
"message": "Usuário não encontrado."
}
Se o usuário não foi encontrado, o status correto deve ser 404.
Uma API deve responder JSON. Se ocorrer erro, o retorno também deve ser JSON.
Evite retornar mensagens como:
SQLSTATE[42S02]: Base table or view not found
Esse tipo de informação deve ir para o log, não para o usuário.
Se a API retorna apenas “erro interno” e você não salva o erro real em log, fica difícil descobrir o problema depois.
Tente separar funções de resposta, classes de exception e validações em arquivos próprios. Isso deixa o projeto mais fácil de manter.
Content-Type está como application/json?401?403?404?500?
Tratar erros em uma API PHP puro é uma etapa fundamental para transformar um projeto simples em uma API mais profissional.
Com respostas JSON padronizadas, status HTTP corretos, uso de try/catch, exceptions personalizadas e logs internos,
sua aplicação fica mais segura, mais fácil de debugar e mais previsível para o frontend.
O ideal é que toda API tenha uma estrutura única para retornar sucesso e erro. Assim, qualquer rota do sistema segue o mesmo padrão, seja em um cadastro, login, consulta, webhook, painel administrativo ou integração externa.
Sim. Uma API deve retornar JSON tanto em respostas de sucesso quanto em respostas de erro. Isso facilita o tratamento no frontend, aplicativo mobile ou integração externa.
Para JSON inválido, o status mais indicado é 400 Bad Request, pois a estrutura da requisição está incorreta.
Para campos inválidos, como e-mail incorreto ou senha curta, o status 422 Unprocessable Entity é uma boa escolha.
O status 401 indica que o usuário não está autenticado ou enviou token inválido.
O status 403 indica que o usuário está autenticado, mas não tem permissão para acessar o recurso.
Não. Em produção, detalhes do banco devem ser registrados em log, mas a resposta pública da API deve retornar uma mensagem genérica.
Não. Mesmo em PHP puro, é possível criar funções, classes e handlers globais para padronizar erros de forma profissional.
Para campos inválidos, como e-mail incorreto ou senha curta, o status 422 Unprocessable Entity é uma boa escolha.