PHP Criar uma API JSON

Até aqui, o PHP da Ana sempre respondeu com páginas HTML, feitas para uma pessoa ler no navegador. Uma API responde com dados, feitos para outro programa ler: um aplicativo de celular, um site em JavaScript, o sistema de um parceiro. O formato mais usado para esses dados é o JSON. Nesta aula, a Ana transforma a lista de produtos da lojinha numa API de verdade e a testa com o curl.

Você vai aprender
  • O que é uma API e o que muda em relação a uma página HTML.
  • Responder JSON: json_encode e o cabeçalho Content-Type: application/json.
  • Descobrir o que o cliente pediu: $_SERVER['REQUEST_METHOD'] e $_SERVER['REQUEST_URI'].
  • Ler um JSON enviado pelo cliente com php://input e json_decode (o $_POST não serve aqui).
  • Os códigos de status 200, 201, 400, 404 e 405, e como enviá-los com http_response_code.
  • Testar cada caso com o curl: GET, POST com JSON, 404 e 405.

A ideia: o balcão e o pedido por escrito

Na lojinha física, a Ana atende pessoas no balcão: mostra os produtos, conversa, embrulha. Isso é a página HTML. Agora imagine que uma escola quer fazer pedidos automaticamente todo mês. Ela não quer "conversar": quer mandar um formulário padronizado e receber de volta outro formulário padronizado, que o sistema dela consiga ler sozinho. Isso é a API (Application Programming Interface, "interface de programação de aplicações").

Uma API em PHP é um script PHP comum, com três diferenças:

  1. Responde JSON em vez de HTML, e avisa isso no cabeçalho Content-Type.
  2. Olha o método (GET = consultar, POST = criar…) e o caminho (/produtos, /produtos/2) para decidir o que fazer. Isso se chama roteamento.
  3. Usa o código de status HTTP para dizer se deu certo (200, 201) ou o que deu errado (404, 400…).

1. A primeira resposta JSON

Comece pelo mais simples: um array do PHP convertido em JSON com json_encode (veja a aula JSON).

<?php
$dados = ['mensagem' => 'Olá, API!', 'versao' => 1];
echo json_encode($dados), "\n";

A Ana liga o servidor embutido num terminal (php -S localhost:8000) e, em outro, pede a página com o curl -i. O -i ("incluir") mostra também os cabeçalhos da resposta, que o navegador esconde:

$ curl -i localhost:8000/ola.php
HTTP/1.1 200 OK
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:20 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-type: text/html; charset=UTF-8

{"mensagem":"Olá, API!","versao":1}

Duas coisas estão erradas:

  • Content-type: text/html: o PHP avisou que a resposta é uma página HTML, porque é o padrão dele. Um cliente que confere o tipo vai recusar ou tratar como texto.
  • Olá: o json_encode escreveu o á como código (á). É JSON válido (quem ler vai entender á), mas fica difícil de ler. A opção JSON_UNESCAPED_UNICODE mantém os acentos.

O cabeçalho certo é enviado com header(), antes de qualquer echo (como em header e Redirecionamento):

<?php
header('Content-Type: application/json; charset=utf-8');

$dados = ['mensagem' => 'Olá, API!', 'versao' => 1];
echo json_encode($dados), "\n";
$ curl -i localhost:8000/ola2.php
HTTP/1.1 200 OK
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:21 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8

{"mensagem":"Olá, API!","versao":1}
header('Content-Type: application/json; charset=utf-8')
Diz ao cliente: "o corpo é JSON, em UTF-8". application/json é o nome oficial do tipo JSON.
json_encode($dados)
Array do PHP → texto JSON. Array associativo vira objeto {…}; lista vira […].
, "\n"
Só para o terminal ficar arrumado (o próximo prompt começa na linha de baixo). O cliente ignora.

As opções do json_encode usadas no resto da aula:

OpçãoEfeito
JSON_UNESCAPED_UNICODEMantém acentos: "Olá" em vez de "Olá".
JSON_UNESCAPED_SLASHESMantém as barras: "/produtos" em vez de "\/produtos".
JSON_PRETTY_PRINTQuebra linhas e indenta, para pessoas lerem. Opcional (os programas não precisam).

Várias opções se juntam com | ("ou" de bits): JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES.


2. O que o cliente pediu: método e caminho

Numa API, a mesma "porta" atende vários pedidos diferentes. Para saber qual é, o PHP oferece, em $_SERVER:

  • $_SERVER['REQUEST_METHOD']: o método (GET, POST, PUT, DELETE…).
  • $_SERVER['REQUEST_URI']: o caminho pedido, com a query string (?ordem=preco).

Para todas as URLs caírem no mesmo arquivo, o servidor embutido aceita um roteador: um script PHP passado depois do endereço, que recebe toda requisição. A Ana fez um "eco", que só devolve o que recebeu:

<?php
header('Content-Type: application/json; charset=utf-8');

echo json_encode([
    'metodo' => $_SERVER['REQUEST_METHOD'],
    'uri'    => $_SERVER['REQUEST_URI'],
    'caminho' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
    'post'   => $_POST,
    'corpo'  => file_get_contents('php://input'),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
$ php -S localhost:8000 eco.php          « eco.php é o roteador: recebe todas as URLs
[Fri Oct 16 20:53:26 2026] PHP 8.5.8 Development Server (http://localhost:8000) started

No outro terminal, o -X do curl escolhe o método:

$ curl localhost:8000/produtos/2?ordem=preco
{
    "metodo": "GET",
    "uri": "/produtos/2?ordem=preco",
    "caminho": "/produtos/2",
    "post": [],
    "corpo": ""
}
$ curl -X DELETE localhost:8000/produtos/2
{
    "metodo": "DELETE",
    "uri": "/produtos/2",
    "caminho": "/produtos/2",
    "post": [],
    "corpo": ""
}
/produtos/2?ordem=preco
Não existe arquivo com esse nome: quem respondeu foi o roteador eco.php.
"uri"
O REQUEST_URI traz a query string junto. Comparar $_SERVER['REQUEST_URI'] === '/produtos' falharia com /produtos?ordem=preco.
parse_url(…, PHP_URL_PATH)
Separa só o caminho (/produtos/2). É ele que usamos para rotear. A query string continua em $_GET.

O corpo da requisição: php://input

Num POST, o cliente manda dados no corpo. Veja a diferença entre mandar um formulário e mandar JSON:

$ curl -X POST localhost:8000/produtos -d 'nome=Lápis&preco=2'
{
    "metodo": "POST",
    "uri": "/produtos",
    "caminho": "/produtos",
    "post": {
        "nome": "Lápis",
        "preco": "2"
    },
    "corpo": "nome=Lápis&preco=2"
}
$ curl -X POST localhost:8000/produtos -H 'Content-Type: application/json' -d '{"nome": "Lápis", "preco": 2}'
{
    "metodo": "POST",
    "uri": "/produtos",
    "caminho": "/produtos",
    "post": [],
    "corpo": "{\"nome\": \"Lápis\", \"preco\": 2}"
}

O -d manda um corpo; o -H acrescenta um cabeçalho. No primeiro caso, o corpo está no formato de formulário (nome=…&preco=…) e o PHP preencheu o $_POST sozinho. No segundo, o corpo é JSON: o PHP não sabe decodificá-lo automaticamente, e o $_POST ficou vazio. O texto cru do corpo está sempre disponível no fluxo especial php://input ("a entrada do PHP"), e é dele que a API lê:

$dados = json_decode(file_get_contents('php://input'), true);
file_get_contents('php://input')
Lê o corpo inteiro da requisição como texto.
json_decode(…, true)
Texto JSON → array associativo. Se o texto não for JSON válido, devolve null.

3. Códigos de status: a resposta em um número

Toda resposta HTTP começa com um código de três dígitos. O cliente olha o número antes de ler o corpo. Por padrão o PHP manda 200; para outro, use http_response_code(404), também antes de qualquer saída.

CódigoNomeQuando usar na API
200OKDeu certo (consulta, alteração).
201Created ("criado")Um POST criou algo. Costuma vir com o cabeçalho Location apontando para o item novo.
204No Content ("sem conteúdo")Deu certo e não há nada para devolver (comum no DELETE).
400Bad Request ("pedido ruim")O cliente mandou dados inválidos ou faltando.
401 / 403Unauthorized / ForbiddenFalta login / está logado mas não tem permissão.
404Not Found ("não encontrado")A rota ou o item não existe.
405Method Not Allowed ("método não permitido")A rota existe, mas não aceita esse método. Mande junto o cabeçalho Allow com os métodos aceitos.
500Internal Server ErrorErro no servidor (o PHP manda sozinho quando há um erro fatal).

Regra prática: 2xx = deu certo; 4xx = o cliente errou; 5xx = o servidor errou. E mesmo no erro, responda JSON ({"erro": "…"}), para o cliente mostrar uma mensagem útil.


4. A API da lojinha

Juntando tudo. Os produtos ficam num arquivo produtos.json (no mundo real, num banco de dados):

[
    {"id": 1, "nome": "Caderno", "preco": 25.5},
    {"id": 2, "nome": "Caneta azul", "preco": 3.9}
]

A API atende três rotas: GET /produtos (lista), POST /produtos (cria) e GET /produtos/{id} (um produto).

<?php

declare(strict_types=1);

const ARQUIVO = __DIR__ . '/produtos.json';

function responder(int $status, array $dados): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
    exit;
}

$produtos = json_decode(file_get_contents(ARQUIVO), true);
$metodo = $_SERVER['REQUEST_METHOD'];
$caminho = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// /produtos
if ($caminho === '/produtos') {
    if ($metodo === 'GET') {
        responder(200, $produtos);
    }

    if ($metodo === 'POST') {
        $dados = json_decode(file_get_contents('php://input'), true);

        if (!is_array($dados) || !isset($dados['nome'], $dados['preco'])) {
            responder(400, ['erro' => 'Envie um JSON com "nome" e "preco"']);
        }

        $novo = [
            'id' => max(array_column($produtos, 'id')) + 1,
            'nome' => (string) $dados['nome'],
            'preco' => (float) $dados['preco'],
        ];
        $produtos[] = $novo;
        file_put_contents(ARQUIVO, json_encode($produtos, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

        header('Location: /produtos/' . $novo['id']);
        responder(201, $novo);
    }

    header('Allow: GET, POST');
    responder(405, ['erro' => "Método $metodo não permitido em /produtos"]);
}

// /produtos/2
if (preg_match('#^/produtos/(\d+)$#', $caminho, $partes)) {
    $id = (int) $partes[1];

    if ($metodo !== 'GET') {
        header('Allow: GET');
        responder(405, ['erro' => "Método $metodo não permitido em /produtos/$id"]);
    }

    foreach ($produtos as $produto) {
        if ($produto['id'] === $id) {
            responder(200, $produto);
        }
    }

    responder(404, ['erro' => "Produto $id não encontrado"]);
}

responder(404, ['erro' => "Rota $caminho não encontrada"]);
TrechoO que faz
function responder(…): neverUm lugar só para responder: status, cabeçalho, JSON e exit. O tipo de retorno never (PHP 8.1+) diz "esta função nunca volta": depois dela, o script acabou. Sem o exit, o código continuaria e poderia mandar uma segunda resposta grudada na primeira.
$caminho === '/produtos'A rota da coleção. Dentro dela, um if por método.
!is_array($dados) || !isset(…)Validação: JSON inválido (json_decode devolveu null) ou sem os campos → 400. Nunca confie no que o cliente manda (veja Validar Dados).
max(array_column($produtos, 'id')) + 1O próximo id: o maior que existe, mais um.
header('Location: …') + 201"Criado, e ele mora aqui."
header('Allow: GET, POST') + 405Rota certa, método errado: diz quais métodos ela aceita.
preg_match('#^/produtos/(\d+)$#', …)Uma expressão regular (um "molde" de texto) que casa /produtos/ seguido só de dígitos. Os parênteses capturam o número em $partes[1].
último responder(404, …)Nenhuma rota casou.

A Ana liga a API com o index.php como roteador:

$ php -S localhost:8000 index.php
[Fri Oct 16 20:53:38 2026] PHP 8.5.8 Development Server (http://localhost:8000) started

Testando com o curl: GET

$ curl localhost:8000/produtos
[
    {
        "id": 1,
        "nome": "Caderno",
        "preco": 25.5
    },
    {
        "id": 2,
        "nome": "Caneta azul",
        "preco": 3.9
    }
]
$ curl -i localhost:8000/produtos/2
HTTP/1.1 200 OK
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8

{
    "id": 2,
    "nome": "Caneta azul",
    "preco": 3.9
}

404: o produto não existe

$ curl -i localhost:8000/produtos/9
HTTP/1.1 404 Not Found
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8

{
    "erro": "Produto 9 não encontrado"
}
$ curl -i localhost:8000/clientes
HTTP/1.1 404 Not Found
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8

{
    "erro": "Rota /clientes não encontrada"
}

POST com JSON: criando um produto

$ curl -i -X POST localhost:8000/produtos -H 'Content-Type: application/json' -d '{"nome": "Borracha", "preco": 1.5}'
HTTP/1.1 201 Created
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Location: /produtos/3
Content-Type: application/json; charset=utf-8

{
    "id": 3,
    "nome": "Borracha",
    "preco": 1.5
}
$ curl localhost:8000/produtos/3
{
    "id": 3,
    "nome": "Borracha",
    "preco": 1.5
}

Status 201 Created, o cabeçalho Location e o produto novo com id 3, que já pode ser consultado. O produtos.json agora tem três produtos.

405 e 400: método errado e dados errados

$ curl -i -X DELETE localhost:8000/produtos
HTTP/1.1 405 Method Not Allowed
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Allow: GET, POST
Content-Type: application/json; charset=utf-8

{
    "erro": "Método DELETE não permitido em /produtos"
}
$ curl -i -X PUT localhost:8000/produtos/1
HTTP/1.1 405 Method Not Allowed
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Allow: GET
Content-Type: application/json; charset=utf-8

{
    "erro": "Método PUT não permitido em /produtos/1"
}
$ curl -i -X POST localhost:8000/produtos -H 'Content-Type: application/json' -d '{nome: Lápis}'
HTTP/1.1 400 Bad Request
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8

{
    "erro": "Envie um JSON com \"nome\" e \"preco\""
}

O {nome: Lápis} parece JSON, mas não é (no JSON, nomes e textos precisam de aspas duplas): o json_decode devolveu null e a validação respondeu 400. As aspas dentro da mensagem aparecem como \" porque é assim que o JSON escreve uma aspa dentro de um texto.

Experimente: o editor do guia não tem curl nem servidor rodando em segundo plano, então as duas primeiras linhas fazem o papel da requisição, preenchendo o $_SERVER à mão. Rode e depois troque o REQUEST_URI por '/produtos', '/produtos/9' e '/clientes?x=1'. A função responder desta caixa mostra também uma linha Status:, que no curl -i apareceria no cabeçalho.

<?php

declare(strict_types=1);

// No editor não existe curl: estas duas linhas fazem o papel da requisição.
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/produtos/2';

function responder(int $status, array $dados): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo "Status: $status\n"; // só no editor: o curl -i mostraria isto no cabeçalho
    echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
    exit;
}

$produtos = [
    ['id' => 1, 'nome' => 'Caderno', 'preco' => 25.5],
    ['id' => 2, 'nome' => 'Caneta azul', 'preco' => 3.9],
];

$metodo = $_SERVER['REQUEST_METHOD'];
$caminho = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($caminho === '/produtos' && $metodo === 'GET') {
    responder(200, $produtos);
}

if (preg_match('#^/produtos/(\d+)$#', $caminho, $partes) && $metodo === 'GET') {
    $id = (int) $partes[1];
    foreach ($produtos as $produto) {
        if ($produto['id'] === $id) {
            responder(200, $produto);
        }
    }
    responder(404, ['erro' => "Produto $id não encontrado"]);
}

responder(404, ['erro' => "Rota $caminho não encontrada"]);

No computador da Ana, com o php do terminal, essa caixa mostra:

$ php api-editor.php
Status: 200
{
    "id": 2,
    "nome": "Caneta azul",
    "preco": 3.9
}

Por que passar o roteador para o php -S

Sem o roteador (php -S localhost:8000, só isso), o servidor embutido entrega qualquer arquivo da pasta que for pedido pelo nome. Para caminhos que não existem, como /produtos, ele procura um index.php, então a API parece funcionar igual. Mas veja:

$ curl localhost:8000/produtos.json         « servidor ligado SEM o roteador
[
    {"id": 1, "nome": "Caderno", "preco": 25.5},
    {"id": 2, "nome": "Caneta azul", "preco": 3.9}
]

O arquivo de dados foi entregue cru, sem passar pela API. Com php -S localhost:8000 index.php, toda requisição passa pelo index.php:

$ curl localhost:8000/produtos.json         « servidor ligado COM o roteador
{
    "erro": "Rota /produtos.json não encontrada"
}

Num servidor de verdade (Apache, Nginx), o mesmo efeito vem da configuração de "reescrita" que manda tudo para o index.php, e os arquivos de dados ficam fora da pasta pública.


Formas diferentes

FormaComoQuando usar
if + preg_match (esta aula)Um bloco por rota.APIs pequenas, aprender como funciona por dentro.
match com o métodomatch ($metodo) { 'GET' => …, 'POST' => …, default => responder(405, …) }Dentro de cada rota, quando há vários métodos (veja match).
Arquivos separadosprodutos.php, clientes.php… e a URL com o nome do arquivo.Muito simples, mas as URLs ficam feias (/produtos.php?id=2).
Micro-frameworksSlim, ou os roteadores do Laravel e do Symfony, instalados com o Composer.Projetos reais: rotas declaradas numa linha, validação, autenticação. Por baixo, fazem o que você fez aqui.

O projeto API REST leva esta API adiante, com SQLite, PUT e DELETE. Para o outro lado (o PHP chamando uma API), veja a próxima aula, Consumir APIs.


Quando dá errado

Um Warning em HTML no meio do JSON
<?php
header('Content-Type: application/json; charset=utf-8');

echo json_encode(['id' => $_GET['id']]), "\n";
$ curl localhost:8000/erro1.php
<br />
<b>Warning</b>:  Undefined array key "id" in <b>/home/ana/api/erro1.php</b> on line <b>4</b><br />
{"id":null}

O que aconteceu: o cliente não mandou ?id=, o PHP avisou "chave de array indefinida" e o aviso saiu no corpo da resposta, em HTML, antes do JSON. Para o cliente, a resposta inteira deixou de ser JSON válido: o json_decode do outro lado devolve null.

Como consertar: valide a entrada ($_GET['id'] ?? null e responda 400 se faltar) e, em produção, desligue o display_errors (veja Boas Práticas).

"Cannot modify header information - headers already sent"
<?php
echo "Começando...\n";
header('Content-Type: application/json; charset=utf-8');

echo json_encode(['ok' => true]), "\n";
$ curl -i localhost:8000/erro2.php
HTTP/1.1 200 OK
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:53:45 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-type: text/html; charset=UTF-8

Começando...
<br />
<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /home/ana/api/erro2.php:2) in <b>/home/ana/api/erro2.php</b> on line <b>3</b><br />
{"ok":true}

O que aconteceu: o echo da linha 2 já mandou o corpo, e com ele os cabeçalhos. O header() da linha 3 chegou tarde ("não dá para mudar os cabeçalhos: já foram enviados, a saída começou em erro2.php:2"), e a resposta saiu como text/html. O mesmo vale para http_response_code. Um espaço ou linha em branco antes do <?php causa o mesmo erro.

Como consertar: nenhuma saída antes de header() e http_response_code(). Por isso a função responder faz os dois antes do echo.

As barras aparecem como \/
$ curl -i -X DELETE localhost:8000/produtos
HTTP/1.1 405 Method Not Allowed
Host: localhost:8000
Date: Fri, 16 Oct 2026 23:52:48 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Allow: GET, POST
Content-Type: application/json; charset=utf-8

{
    "erro": "Método DELETE não permitido em \/produtos"
}

O que aconteceu: esta resposta veio de uma versão anterior da API, sem a opção JSON_UNESCAPED_SLASHES. Por padrão, o json_encode escreve / como \/. Não é erro (o cliente lê /produtos do mesmo jeito), só fica feio.

Como consertar: acrescente JSON_UNESCAPED_SLASHES às opções do json_encode.

O POST chega, mas $_POST está vazio

O que aconteceu: o cliente mandou JSON (Content-Type: application/json). O PHP só preenche o $_POST para corpos de formulário. Você viu isso no eco.php.

Como consertar: leia json_decode(file_get_contents('php://input'), true).


Referência rápida

O quêComo
Dizer que a resposta é JSONheader('Content-Type: application/json; charset=utf-8');
Array → JSONjson_encode($dados, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
Código de statushttp_response_code(404); (antes de qualquer saída)
Método pedido$_SERVER['REQUEST_METHOD']
Caminho pedidoparse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
Corpo JSON enviadojson_decode(file_get_contents('php://input'), true)
Parte variável da rotapreg_match('#^/produtos/(\d+)$#', $caminho, $partes) → $partes[1]
Métodos aceitos (com 405)header('Allow: GET, POST');
Onde mora o item criado (com 201)header('Location: /produtos/3');
Servidor com roteadorphp -S localhost:8000 index.php
Testarcurl -i URL, curl -X POST URL -H 'Content-Type: application/json' -d '{…}'

Exercícios

1. O tipo da resposta

Complete o cabeçalho que diz que a resposta é JSON:

header('Content-Type: ');

2. Ler o corpo

Complete para ler o JSON enviado no corpo da requisição:

$dados = json_decode(file_get_contents(''), true);

3. Produto não encontrado

Complete com o código de status certo:

http_response_code(); // não existe produto com esse id

4. Um POST /produtos criou o produto 7. Qual a melhor resposta?

5. Com curl -X POST … -H 'Content-Type: application/json' -d '{"nome":"Lápis"}', o que o PHP coloca em $_POST?

6. Para a URL /produtos?ordem=preco, o que vale $_SERVER['REQUEST_URI']?

Missão final: a requisição simulada agora é DELETE /produtos, e a API responde 404 Rota /produtos não encontrada, o que está errado: a rota existe, o método é que não é aceito. Mude o primeiro if para que, em /produtos, o GET continue devolvendo a lista e qualquer outro método receba 405 com {"erro": "Método DELETE não permitido"} (use a variável $metodo na mensagem) e o cabeçalho Allow: GET.

<?php

declare(strict_types=1);

// No editor não existe curl: estas duas linhas fazem o papel da requisição.
$_SERVER['REQUEST_METHOD'] = 'DELETE';
$_SERVER['REQUEST_URI'] = '/produtos';

function responder(int $status, array $dados): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo "Status: $status\n"; // só no editor: o curl -i mostraria isto no cabeçalho
    echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
    exit;
}

$produtos = [
    ['id' => 1, 'nome' => 'Caderno', 'preco' => 25.5],
    ['id' => 2, 'nome' => 'Caneta azul', 'preco' => 3.9],
];

$metodo = $_SERVER['REQUEST_METHOD'];
$caminho = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($caminho === '/produtos' && $metodo === 'GET') {
    responder(200, $produtos);
}

if (preg_match('#^/produtos/(\d+)$#', $caminho, $partes) && $metodo === 'GET') {
    $id = (int) $partes[1];
    foreach ($produtos as $produto) {
        if ($produto['id'] === $id) {
            responder(200, $produto);
        }
    }
    responder(404, ['erro' => "Produto $id não encontrado"]);
}

responder(404, ['erro' => "Rota $caminho não encontrada"]);
<?php

declare(strict_types=1);

// No editor não existe curl: estas duas linhas fazem o papel da requisição.
$_SERVER['REQUEST_METHOD'] = 'DELETE';
$_SERVER['REQUEST_URI'] = '/produtos';

function responder(int $status, array $dados): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo "Status: $status\n"; // só no editor: o curl -i mostraria isto no cabeçalho
    echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
    exit;
}

$produtos = [
    ['id' => 1, 'nome' => 'Caderno', 'preco' => 25.5],
    ['id' => 2, 'nome' => 'Caneta azul', 'preco' => 3.9],
];

$metodo = $_SERVER['REQUEST_METHOD'];
$caminho = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($caminho === '/produtos') {
    if ($metodo === 'GET') {
        responder(200, $produtos);
    }
    header('Allow: GET');
    responder(405, ['erro' => "Método $metodo não permitido"]);
}

if (preg_match('#^/produtos/(\d+)$#', $caminho, $partes) && $metodo === 'GET') {
    $id = (int) $partes[1];
    foreach ($produtos as $produto) {
        if ($produto['id'] === $id) {
            responder(200, $produto);
        }
    }
    responder(404, ['erro' => "Produto $id não encontrado"]);
}

responder(404, ['erro' => "Rota $caminho não encontrada"]);

Resumo

  • Uma API responde dados (JSON) para programas, não páginas para pessoas.
  • header('Content-Type: application/json; charset=utf-8') + json_encode (com JSON_UNESCAPED_UNICODE e JSON_UNESCAPED_SLASHES).
  • Roteie pelo método ($_SERVER['REQUEST_METHOD']) e pelo caminho (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)).
  • JSON enviado pelo cliente não vai para o $_POST: leia com json_decode(file_get_contents('php://input'), true) e valide.
  • O status conta o resultado: 200 OK, 201 criado, 400 dados ruins, 404 não existe, 405 método não aceito (com Allow). Mesmo no erro, responda JSON.
  • header() e http_response_code() vêm antes de qualquer saída; avisos do PHP no meio da resposta estragam o JSON.
  • Teste tudo com curl -i, com o servidor ligado assim: php -S localhost:8000 index.php.