Makrosites

Suas ideias em realidade digital!

Tratamento de erros em API PHP: resposta JSON padronizada para 400, 401, 403, 404 e 500

Tratamento de erros em API PHP: resposta JSON padronizada para 400, 401, 403, 404 e 500 >

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.

Resumo prático: uma API profissional deve retornar sempre JSON, usar o código HTTP correto e esconder detalhes internos do servidor quando ocorrer um erro inesperado.

Por que padronizar erros em uma API PHP?

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:

Uma boa estrutura de erro melhora a segurança, facilita o consumo da API e deixa o sistema muito mais profissional.

Modelo ideal de resposta JSON para erro

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.

Função base para retornar JSON em PHP

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.'
]);

Status HTTP mais usados em APIs PHP

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.

Erro 400 em API PHP: requisição inválida

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.

Erro 401 em API PHP: não autenticado

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.'
    ]);
}
Dica: use 401 quando o problema for autenticação. Se o usuário estiver autenticado, mas não tiver permissão, use 403.

Erro 403 em API PHP: sem permissão

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.

Erro 404 em API PHP: rota ou recurso não encontrado

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.'
]);

Erro 422 em API PHP: campos inválidos

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.

Erro 500 em API PHP: falha interna no servidor

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.'
    ]);
}
Atenção: mensagens como nome da tabela, usuário do banco, caminho de arquivos e detalhes de SQL não devem aparecer na resposta pública da API.

Criando uma função para erro padrão

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.'
]);

Criando uma classe ApiException

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.

Exemplo usando try/catch com ApiException

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.

Tratando erros fatais e exceptions não capturadas

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.

Usando debug somente em ambiente de desenvolvimento

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.

Estrutura sugerida para organizar erros em PHP puro

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.'
    ]);
});

Exemplo completo de endpoint com tratamento de erros

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.');
}

Diferença entre erro de validação e erro interno

É 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

Erros comuns no tratamento de APIs PHP

1. Retornar status 200 para erro

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.

2. Retornar erro em HTML

Uma API deve responder JSON. Se ocorrer erro, o retorno também deve ser JSON.

3. Exibir erro real do servidor

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.

4. Não registrar logs

Se a API retorna apenas “erro interno” e você não salva o erro real em log, fica difícil descobrir o problema depois.

5. Misturar regra de negócio com tratamento de erro

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.

Checklist para tratamento de erros em API PHP

Conclusão

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.


Perguntas frequentes sobre tratamento de erros em API PHP

Uma API PHP deve retornar erro em JSON?

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.

Qual status usar para JSON inválido?

Para JSON inválido, o status mais indicado é 400 Bad Request, pois a estrutura da requisição está incorreta.

Qual status usar para campos inválidos?

Para campos inválidos, como e-mail incorreto ou senha curta, o status 422 Unprocessable Entity é uma boa escolha.

Qual a diferença entre 401 e 403?

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.

Devo mostrar o erro real do banco na resposta da API?

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.

Preciso usar framework para tratar erros corretamente?

Não. Mesmo em PHP puro, é possível criar funções, classes e handlers globais para padronizar erros de forma profissional.

Qual status usar para campos inválidos?

Para campos inválidos, como e-mail incorreto ou senha curta, o status 422 Unprocessable Entity é uma boa escolha.