Projeto API REST
Até aqui, seus programas PHP devolviam páginas HTML para pessoas. Uma API devolve dados (em JSON) para outros programas: um aplicativo de celular, um site em JavaScript, um script. Neste projeto você vai construir a API das tarefas da Ana: listar, criar, marcar como feita e apagar, cada ação com o verbo HTTP certo e o código de status certo. Tudo testado com o curl, o "navegador de terminal".
- Responder em JSON com
header,json_encodeehttp_response_code. - Guardar as tarefas no SQLite com PDO e prepared statements.
- Ler
$_SERVER["REQUEST_METHOD"]e$_SERVER["PATH_INFO"]para escolher a ação (referência de $_SERVER). - Ler um corpo JSON com
php://inpute validar o que chegou. - Tratar erros: 404, 405, 422 e um 500 que não mostra as entranhas do servidor.
O que vamos construir
Uma API com um único arquivo de entrada, api.php, que entende estes pedidos:
| Pedido | O que faz | Responde |
|---|---|---|
GET /api.php/tarefas | Lista todas as tarefas | 200 + a lista |
GET /api.php/tarefas/2 | Mostra a tarefa 2 | 200 + a tarefa, ou 404 |
POST /api.php/tarefas | Cria uma tarefa (o título vai no corpo) | 201 + a tarefa criada, ou 422 |
PUT /api.php/tarefas/2 | Muda o título e/ou marca como feita | 200 + a tarefa, 404 ou 422 |
DELETE /api.php/tarefas/2 | Apaga a tarefa 2 | 204 (sem corpo), ou 404 |
A API pronta, em uso (o servidor php -S roda em outro terminal):
$ curl -s localhost:8000/api.php/tarefas
[
{
"id": 1,
"titulo": "Comprar pão",
"feita": true
},
{
"id": 2,
"titulo": "Estudar PHP",
"feita": false
}
]
$ curl -s localhost:8000/api.php/tarefas -H 'Content-Type: application/json' -d '{"titulo": "Pagar a conta de luz"}'
{
"id": 3,
"titulo": "Pagar a conta de luz",
"feita": false
}
$ curl -s -X PUT localhost:8000/api.php/tarefas/3 -H 'Content-Type: application/json' -d '{"feita": true}'
{
"id": 3,
"titulo": "Pagar a conta de luz",
"feita": true
}
$ curl -si -X DELETE localhost:8000/api.php/tarefas/1 | head -1
HTTP/1.1 204 No Content
$ curl -s localhost:8000/api.php/tarefas/1
{
"erro": "Tarefa 1 não encontrada"
}
E o terminal do servidor registra cada pedido com o seu código de status:
$ php -S localhost:8000 [Fri Oct 16 21:04:58 2026] PHP 8.5.8 Development Server (http://localhost:8000) started [Fri Oct 16 21:04:59 2026] 127.0.0.1:41336 Accepted [Fri Oct 16 21:04:59 2026] 127.0.0.1:41336 [200]: GET /api.php/tarefas [Fri Oct 16 21:04:59 2026] 127.0.0.1:41336 Closing [Fri Oct 16 21:04:59 2026] 127.0.0.1:41350 Accepted [Fri Oct 16 21:04:59 2026] 127.0.0.1:41350 [201]: POST /api.php/tarefas [Fri Oct 16 21:04:59 2026] 127.0.0.1:41350 Closing [Fri Oct 16 21:04:59 2026] 127.0.0.1:41366 Accepted [Fri Oct 16 21:04:59 2026] 127.0.0.1:41366 [200]: PUT /api.php/tarefas/3 [Fri Oct 16 21:04:59 2026] 127.0.0.1:41366 Closing [Fri Oct 16 21:04:59 2026] 127.0.0.1:41370 Accepted [Fri Oct 16 21:04:59 2026] 127.0.0.1:41370 [204]: DELETE /api.php/tarefas/1 [Fri Oct 16 21:04:59 2026] 127.0.0.1:41370 Closing [Fri Oct 16 21:04:59 2026] 127.0.0.1:41376 Accepted [Fri Oct 16 21:04:59 2026] 127.0.0.1:41376 [404]: GET /api.php/tarefas/1 [Fri Oct 16 21:04:59 2026] 127.0.0.1:41376 Closing
- Responder em JSON (
header,json_encode,http_response_code). - O banco e as rotas GET (SQLite,
PATH_INFO, 404). - Criar com POST (
php://input, validação, 201, 422). - Atualizar com PUT e apagar com DELETE (204, 405).
- Erros inesperados (500 em JSON com
set_exception_handler).
Antes de começar: a ideia do REST
Pense num balcão de atendimento. Você diz o que quer fazer (consultar, cadastrar, alterar, cancelar) e com o quê (a ficha número 2). No HTTP, o "o que fazer" é o método (ou verbo) e o "com o quê" é o endereço (o recurso). REST é só essa combinação, usada de um jeito previsível:
| Método | Significado | Tem corpo? |
|---|---|---|
GET | "me mostre" (nunca muda nada) | não |
POST | "crie um novo" | sim |
PUT | "altere este" | sim |
DELETE | "apague este" | não |
E a resposta sempre traz um código de status, um número de três dígitos que o programa do outro lado lê antes de tudo:
| Código | Nome | Nesta API, quando |
|---|---|---|
200 | OK | deu certo e há dados na resposta |
201 | Created (criado) | um POST criou uma tarefa |
204 | No Content (sem conteúdo) | apagou; não há nada para mostrar |
400 | Bad Request (pedido mal feito) | o JSON enviado está quebrado |
404 | Not Found (não encontrado) | a tarefa ou o endereço não existe |
405 | Method Not Allowed | método que o endereço não aceita (DELETE /tarefas) |
422 | Unprocessable Content (não processável) | o JSON está certo, mas os dados não (título vazio) |
500 | Internal Server Error | um erro no nosso código |
Regra para lembrar: 2xx = deu certo; 4xx = o cliente errou; 5xx = o servidor errou.
O curl
Navegadores só fazem GET pela barra de endereço. Para testar os outros métodos, usamos o curl no terminal (veja também O Servidor Embutido):
| Opção | O que faz |
|---|---|
-s | silent: sem a barra de progresso |
-i | mostra também os cabeçalhos da resposta (a linha HTTP/1.1 200 OK e as seguintes) |
-X PUT | escolhe o método (sem -X: GET, ou POST se houver -d) |
-H 'Nome: valor' | manda um cabeçalho, como Content-Type: application/json ("o corpo é JSON") |
-d 'dados' | manda um corpo (e, sozinho, faz um POST) |
A estrutura do projeto, na pasta ~/api:
$ ls api.php banco.php tarefas.sqlite « criado sozinho na primeira requisição
O editor do guia roda o PHP de verdade em modo web, mas, como um formulário HTML, ele só sabe enviar GET (pelo endereço, data-url) e POST com campos de formulário. Por isso a API aceita também corpo de formulário, e na etapa 4 você vai ver o truque do campo _method para experimentar PUT e DELETE no editor. Todos os testes com JSON, PUT e DELETE desta página foram feitos com curl na máquina da Ana.
Etapa 1 · Responder em JSON
A primeira versão é ingênua: monta um array e o transforma em texto JSON com json_encode.
<?php
$tarefas = [
["id" => 1, "titulo" => "Comprar pão", "feita" => true],
["id" => 2, "titulo" => "Estudar PHP", "feita" => false],
];
echo json_encode($tarefas);
Com curl -si, olhe a resposta inteira, cabeçalhos incluídos:
$ curl -si localhost:8000/api.php
HTTP/1.1 200 OK
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:08:25 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-type: text/html; charset=UTF-8
[{"id":1,"titulo":"Comprar pão","feita":true},{"id":2,"titulo":"Estudar PHP","feita":false}]
Funciona, mas com três defeitos:
Content-type: text/html: o PHP avisa ao cliente que mandou uma página HTML. Muitos programas confiam nesse cabeçalho para saber como ler a resposta.pão: por padrão, ojson_encodetroca letras acentuadas por códigos\u…. É JSON válido (qualquer programa entende), mas ilegível para nós.- Tudo numa linha só e sem quebra no fim: difícil de ler no terminal.
A versão certa põe a resposta numa função que vai ser usada em todo o projeto:
<?php
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
$tarefas = [
["id" => 1, "titulo" => "Comprar pão", "feita" => true],
["id" => 2, "titulo" => "Estudar PHP", "feita" => false],
];
responder(200, $tarefas);
| Linha | O que faz |
|---|---|
function responder(int $status, mixed $dados = null): never | Recebe o código de status e os dados (de qualquer tipo: mixed). O tipo de retorno never PHP 8.1+ diz "esta função nunca volta": ela sempre termina o programa. Veja Tipos e Retorno. |
http_response_code($status); | Define o código de status da resposta (sem isto, é sempre 200). |
header("Content-Type: application/json; charset=utf-8"); | Avisa que o corpo é JSON, em UTF-8. Cabeçalhos têm de sair antes de qualquer echo (header). |
JSON_PRETTY_PRINT | Uma informação por linha, com recuo de 4 espaços. |
JSON_UNESCAPED_UNICODE | Mantém ã como ã. |
JSON_UNESCAPED_SLASHES | Mantém / como / (sem ele, /tarefas sairia \/tarefas). |
exit; | Termina o programa: nada depois de responder(…) roda. Assim, cada rota pode simplesmente "responder e acabar". |
As opções do json_encode se somam com | (o "ou" de bits: liga as três). Veja JSON.
$ curl -si localhost:8000/api.php
HTTP/1.1 200 OK
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:02 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8
[
{
"id": 1,
"titulo": "Comprar pão",
"feita": true
},
{
"id": 2,
"titulo": "Estudar PHP",
"feita": false
}
]
Repare nos tipos: 1 sem aspas é número, "Comprar pão" é texto, true/false são booleanos. O json_encode converte cada tipo do PHP no tipo equivalente do JSON, e uma lista PHP ([…, …]) vira uma lista JSON.
Missão (etapa 1): rode e veja "Comprar pão" no resultado. Acrescente as opções que faltam no json_encode para que apareça "Comprar pão".
<?php
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT), "\n";
}
exit;
}
$tarefas = [
["id" => 1, "titulo" => "Comprar pão", "feita" => true],
["id" => 2, "titulo" => "Estudar PHP", "feita" => false],
];
responder(200, $tarefas);
<?php
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
$tarefas = [
["id" => 1, "titulo" => "Comprar pão", "feita" => true],
["id" => 2, "titulo" => "Estudar PHP", "feita" => false],
];
responder(200, $tarefas);
Etapa 2 · O banco e as rotas GET
As tarefas agora vão para um banco SQLite (um arquivo só, sem servidor; veja Banco de Dados). A conexão fica num arquivo separado, que o api.php inclui (include e require):
<?php
$arquivo = __DIR__ . "/tarefas.sqlite";
$novo = !file_exists($arquivo);
$pdo = new PDO("sqlite:$arquivo");
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if ($novo) {
$pdo->exec("CREATE TABLE tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO tarefas (titulo, feita) VALUES ('Comprar pão', 1), ('Estudar PHP', 0)");
}
return $pdo;
| Linha | O que faz |
|---|---|
__DIR__ . "/tarefas.sqlite" | O arquivo do banco fica na mesma pasta do banco.php, não importa de onde o PHP foi chamado. |
$novo = !file_exists($arquivo); | Guarda se o banco ainda não existia (o SQLite cria o arquivo vazio ao conectar). |
PDO::FETCH_ASSOC | Cada linha do banco vem como array associativo: ["id" => 1, "titulo" => "Comprar pão", "feita" => 1]. |
CREATE TABLE … | Só na primeira vez: cria a tabela. AUTOINCREMENT = o banco escolhe o próximo número. O SQLite não tem tipo booleano: feita é 0 ou 1. |
INSERT … | Duas tarefas de exemplo, para a API não começar vazia. |
return $pdo; | Um arquivo incluído pode devolver um valor: $pdo = require __DIR__ . "/banco.php";. |
Rotas: método + caminho
Como o api.php sabe se o pedido foi /tarefas ou /tarefas/2? Quando o endereço continua depois do nome do arquivo (/api.php/tarefas/2), o PHP põe esse pedaço em $_SERVER["PATH_INFO"]. Junto com $_SERVER["REQUEST_METHOD"], isso escolhe a rota:
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
$caminho = $_SERVER["PATH_INFO"] ?? "/";
if ($caminho === "/tarefas" && $metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes) && $metodo === "GET") {
$id = (int) $partes[1];
responder(200, formatar(buscar($pdo, $id)));
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
| Trecho | O que faz |
|---|---|
$_SERVER["PATH_INFO"] ?? "/" | Em /api.php/tarefas/2, vale "/tarefas/2". Em /api.php puro, não existe: usamos "/". |
preg_match('#^/tarefas/(\d+)$#', $caminho, $partes) | Confere se o caminho é /tarefas/ seguido de dígitos (\d+) e nada mais (^ = começo, $ = fim). O que está entre parênteses é guardado em $partes[1]: "2". O # é só o delimitador da expressão (no lugar da /, que aparece no caminho). |
(int) $partes[1] | Converte "2" em 2. Como a expressão só aceita dígitos, /tarefas/abc nem chega aqui. |
responder(404, …) no fim | Se nenhuma rota respondeu (lembre: responder termina o programa), o endereço não existe. |
E duas funções novas:
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
formatarconverte a linha do banco no formato da API:feitasai comotrue/falseem vez de1/0. Comarray_map("formatar", $tarefas), ela é aplicada a cada linha (array_map).buscarusa um prepared statement: o?é preenchido peloexecute, nunca colado no SQL. Mesmo que o id viesse do visitante sem conversão, não haveria SQL Injection (aula).fetch()devolve a linha oufalsese não achou: aí respondemos 404.
$ ls
api.php
banco.php
$ curl -s localhost:8000/api.php/tarefas
[
{
"id": 1,
"titulo": "Comprar pão",
"feita": true
},
{
"id": 2,
"titulo": "Estudar PHP",
"feita": false
}
]
$ ls
api.php
banco.php
tarefas.sqlite
$ curl -s localhost:8000/api.php/tarefas/2
{
"id": 2,
"titulo": "Estudar PHP",
"feita": false
}
$ curl -si localhost:8000/api.php/tarefas/99
HTTP/1.1 404 Not Found
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:03 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8
{
"erro": "Tarefa 99 não encontrada"
}
$ curl -s localhost:8000/api.php/usuarios
{
"erro": "Rota não encontrada: GET /usuarios"
}
Repare: o tarefas.sqlite apareceu depois da primeira requisição. E um erro também é respondido em JSON, com o código certo: quem usa a API lê o 404 e a mensagem em "erro".
Sem o if ($tarefa === false), a função tenta devolver false onde prometeu um array:
$ curl -si localhost:8000/api.php/tarefas/99
HTTP/1.1 200 OK
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:08:26 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-type: text/html; charset=UTF-8
<br />
<b>Fatal error</b>: Uncaught TypeError: buscar(): Return value must be of type array, false returned in /home/ana/api/api.php:23
Stack trace:
#0 /home/ana/api/api.php(37): buscar(Object(PDO), 99)
#1 {main}
thrown in <b>/home/ana/api/api.php</b> on line <b>23</b><br />
O cliente esperava JSON e recebeu HTML com um erro, e ainda com status 200 (o php -S enviou a mensagem de erro como uma página normal). Como consertar: todo fetch() pode devolver false; trate esse caso. A mensagem está explicada na referência de erros.
Missão (etapa 2): a caixa pede GET /api.php/tarefas/99 e a função buscar estoura. Faça-a responder 404 com {"erro": "Tarefa 99 não encontrada"}. Depois troque o endereço na barra do resultado para /api.php/tarefas e /api.php/tarefas/2.
<?php
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
// e se a tarefa não existir? $st->fetch() devolve false…
return $tarefa;
}
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
$caminho = $_SERVER["PATH_INFO"] ?? "/";
if ($caminho === "/tarefas" && $metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes) && $metodo === "GET") {
$id = (int) $partes[1];
responder(200, formatar(buscar($pdo, $id)));
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
<?php
$arquivo = __DIR__ . "/tarefas.sqlite";
$novo = !file_exists($arquivo);
$pdo = new PDO("sqlite:$arquivo");
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if ($novo) {
$pdo->exec("CREATE TABLE tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO tarefas (titulo, feita) VALUES ('Comprar pão', 1), ('Estudar PHP', 0)");
}
return $pdo;
<?php
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
$caminho = $_SERVER["PATH_INFO"] ?? "/";
if ($caminho === "/tarefas" && $metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes) && $metodo === "GET") {
$id = (int) $partes[1];
responder(200, formatar(buscar($pdo, $id)));
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
Etapa 3 · Criar com POST
Para criar, o cliente manda um POST /tarefas com o título no corpo. APIs costumam receber o corpo em JSON ({"titulo": "Lavar a louça"}), e aí vem uma surpresa: o PHP só preenche $_POST para corpos de formulário. Um corpo JSON tem de ser lido "cru", de php://input (um arquivo especial com o corpo do pedido), e decodificado com json_decode:
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
| Trecho | O que faz |
|---|---|
$_SERVER["CONTENT_TYPE"] | O cabeçalho Content-Type do pedido: diz em que formato está o corpo. |
file_get_contents("php://input") | O corpo inteiro, como texto. |
json_decode(…, true) | Texto JSON → array associativo (true = array, não objeto). JSON quebrado dá null; um JSON válido que não é objeto ("oi", 42) não é array. Os dois casos: 400. |
return $_POST; | Se não é JSON, aceitamos um corpo de formulário (é o que o editor do guia e os formulários HTML enviam). |
!is_string($titulo) || trim($titulo) === "" | O título tem de ser texto (o cliente pode mandar {"titulo": 42} ou {"titulo": ["a"]}) e não pode ser só espaços. |
mb_strlen($titulo) > 100 | Um limite razoável, contando letras (não bytes; veja bytes × letras). |
E a rota:
if ($caminho === "/tarefas" && $metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
lastInsertId() devolve o id que o banco acabou de escolher (como texto; por isso o (int)). A resposta de criação segue a convenção do HTTP: código 201 Created, o cabeçalho Location com o endereço da coisa nova, e a tarefa criada no corpo (o cliente fica sabendo o id sem perguntar de novo).
$ curl -si localhost:8000/api.php/tarefas -H 'Content-Type: application/json' -d '{"titulo": "Lavar a louça"}'
HTTP/1.1 201 Created
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:06 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Location: /api.php/tarefas/3
Content-Type: application/json; charset=utf-8
{
"id": 3,
"titulo": "Lavar a louça",
"feita": false
}
$ curl -s localhost:8000/api.php/tarefas/3
{
"id": 3,
"titulo": "Lavar a louça",
"feita": false
}
$ curl -si localhost:8000/api.php/tarefas -H 'Content-Type: application/json' -d '{"titulo": " "}'
HTTP/1.1 422 Unknown Status Code
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:06 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8
{
"erro": "O título é obrigatório"
}
$ curl -s localhost:8000/api.php/tarefas -H 'Content-Type: application/json' -d '{titulo: Lavar}'
{
"erro": "JSON inválido"
}
$ curl -s localhost:8000/api.php/tarefas -d 'titulo=Regar as plantas'
{
"id": 4,
"titulo": "Regar as plantas",
"feita": false
}
$ sqlite3 tarefas.sqlite "SELECT * FROM tarefas"
1|Comprar pão|1
2|Estudar PHP|0
3|Lavar a louça|0
4|Regar as plantas|0
422 Unknown Status Code: o código chegou certo (422); só o nome ao lado é que o servidor de desenvolvimento do PHP não conhece. Programas olham o número, não o nome. Apache e nginx escrevemUnprocessable Content.{titulo: Lavar}não é JSON (faltam as aspas duplas):400.- O último POST mandou um corpo de formulário (
-dsemContent-Typede JSON): caiu no$_POSTe funcionou também. - O
sqlite3(o programa de terminal do SQLite) confirma o que está gravado:feitaé1/0no banco etrue/falsena API.
Sem validarTitulo, usando $dados["titulo"] ?? "" direto, a API aceita qualquer coisa, inclusive um título vazio ou um campo com o nome errado:
$ curl -s localhost:8000/api.php/tarefas -H 'Content-Type: application/json' -d '{"titulo": ""}'
{
"id": 3,
"titulo": "",
"feita": false
}
$ curl -s localhost:8000/api.php/tarefas -H 'Content-Type: application/json' -d '{"nome": "Lavar"}'
{
"id": 4,
"titulo": "",
"feita": false
}
O banco agora tem tarefas sem título. Uma API nunca confia no cliente: valide tipo, presença e tamanho de tudo o que chega, e responda 422 explicando o problema.
Missão (etapa 3): a caixa faz um POST com o título vazio (titulo=) e a API o aceita. Escreva a função validarTitulo (texto, sem ser só espaços, até 100 letras) e use-a na rota, para responder 422 com {"erro": "O título é obrigatório"}.
<?php
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
$caminho = $_SERVER["PATH_INFO"] ?? "/";
if ($caminho === "/tarefas" && $metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($caminho === "/tarefas" && $metodo === "POST") {
$dados = lerCorpo();
$titulo = $dados["titulo"] ?? ""; // falta validar!
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes) && $metodo === "GET") {
$id = (int) $partes[1];
responder(200, formatar(buscar($pdo, $id)));
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
<?php
$arquivo = __DIR__ . "/tarefas.sqlite";
$novo = !file_exists($arquivo);
$pdo = new PDO("sqlite:$arquivo");
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if ($novo) {
$pdo->exec("CREATE TABLE tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO tarefas (titulo, feita) VALUES ('Comprar pão', 1), ('Estudar PHP', 0)");
}
return $pdo;
<?php
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
$caminho = $_SERVER["PATH_INFO"] ?? "/";
if ($caminho === "/tarefas" && $metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($caminho === "/tarefas" && $metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes) && $metodo === "GET") {
$id = (int) $partes[1];
responder(200, formatar(buscar($pdo, $id)));
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
Etapa 4 · Atualizar com PUT e apagar com DELETE
As duas ações agem sobre uma tarefa, então ficam na rota /tarefas/{id}. Para organizar, cada caminho testa os métodos que aceita e, no fim, responde 405 para os outros:
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "PUT") {
$tarefa = buscar($pdo, $id);
$dados = lerCorpo();
$titulo = array_key_exists("titulo", $dados) ? validarTitulo($dados["titulo"]) : $tarefa["titulo"];
$feita = array_key_exists("feita", $dados) ? (bool) $dados["feita"] : (bool) $tarefa["feita"];
$st = $pdo->prepare("UPDATE tarefas SET titulo = ?, feita = ? WHERE id = ?");
$st->execute([$titulo, (int) $feita, $id]);
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
| Trecho | O que faz |
|---|---|
$tarefa = buscar($pdo, $id); (no PUT e no DELETE) | Primeiro confere se a tarefa existe: se não, buscar já responde 404 e o programa para. |
array_key_exists("titulo", $dados) ? … : $tarefa["titulo"] | O cliente pode mandar só o que quer mudar. Campo ausente = mantém o valor atual (ternário). |
(bool) $dados["feita"] | Aceita true/false do JSON e "1"/"0" de formulário. |
responder(204); | Sem dados: 204 No Content, corpo vazio. (A nossa responder só escreve o JSON se $dados não for null.) |
header("Allow: …"); responder(405, …) | Método que este endereço não aceita. O cabeçalho Allow conta quais são aceitos. A rota /tarefas ganhou o mesmo final, com Allow: GET, POST. |
A rigor, PUT substitui o recurso inteiro e PATCH muda só uma parte, que é o que o nosso PUT faz. Muitas APIs aceitam os dois; aceitar PATCH também é um dos desafios.
$ curl -si -X PUT localhost:8000/api.php/tarefas/2 -H 'Content-Type: application/json' -d '{"feita": true}'
HTTP/1.1 200 OK
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:18 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8
{
"id": 2,
"titulo": "Estudar PHP",
"feita": true
}
$ curl -s -X PUT localhost:8000/api.php/tarefas/2 -H 'Content-Type: application/json' -d '{"titulo": "Estudar PHP e SQL"}'
{
"id": 2,
"titulo": "Estudar PHP e SQL",
"feita": true
}
$ curl -si -X DELETE localhost:8000/api.php/tarefas/1
HTTP/1.1 204 No Content
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:18 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-type: text/html; charset=UTF-8
$ curl -s -X DELETE localhost:8000/api.php/tarefas/1
{
"erro": "Tarefa 1 não encontrada"
}
$ curl -si -X PATCH localhost:8000/api.php/tarefas/2
HTTP/1.1 405 Method Not Allowed
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:18 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Allow: GET, PUT, DELETE
Content-Type: application/json; charset=utf-8
{
"erro": "Método PATCH não permitido em /tarefas/2"
}
$ curl -s -X DELETE localhost:8000/api.php/tarefas
{
"erro": "Método DELETE não permitido em /tarefas"
}
Apagar duas vezes a mesma tarefa dá 404 na segunda: ela já não existe. (O Content-type: text/html do 204 é o padrão do PHP; como não há corpo, não faz diferença.)
O truque do _method (formulários e o editor do guia)
Formulários HTML só enviam GET e POST. Frameworks como Laravel resolvem isso com um campo oculto _method: um POST com _method=PUT é tratado como PUT. Três linhas, logo depois de ler o método:
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$ curl -s localhost:8000/api.php/tarefas/2 -d '_method=PUT&feita=0'
{
"id": 2,
"titulo": "Estudar PHP e SQL",
"feita": false
}
$ curl -s localhost:8000/api.php/tarefas
[
{
"id": 2,
"titulo": "Estudar PHP e SQL",
"feita": false
}
]
Missão (etapa 4): a caixa envia _method=PUT&feita=1 para /api.php/tarefas/2, mas o PUT ainda não existe (a API responde 405). Escreva o bloco do PUT: busque a tarefa, leia o corpo, mantenha o que não veio, grave com UPDATE (prepared statement) e responda 200 com a tarefa atualizada ("feita": true).
<?php
// ---------- Funções de apoio ----------
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
// ---------- A requisição ----------
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$caminho = $_SERVER["PATH_INFO"] ?? "/";
// ---------- Rotas ----------
if ($caminho === "/tarefas") {
if ($metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
header("Allow: GET, POST");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
// PUT: atualize "titulo" e/ou "feita" e responda 200 com a tarefa atualizada
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
<?php
$arquivo = __DIR__ . "/tarefas.sqlite";
$novo = !file_exists($arquivo);
$pdo = new PDO("sqlite:$arquivo");
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if ($novo) {
$pdo->exec("CREATE TABLE tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO tarefas (titulo, feita) VALUES ('Comprar pão', 1), ('Estudar PHP', 0)");
}
return $pdo;
<?php
// ---------- Funções de apoio ----------
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
// ---------- A requisição ----------
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$caminho = $_SERVER["PATH_INFO"] ?? "/";
// ---------- Rotas ----------
if ($caminho === "/tarefas") {
if ($metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
header("Allow: GET, POST");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "PUT") {
$tarefa = buscar($pdo, $id);
$dados = lerCorpo();
$titulo = array_key_exists("titulo", $dados) ? validarTitulo($dados["titulo"]) : $tarefa["titulo"];
$feita = array_key_exists("feita", $dados) ? (bool) $dados["feita"] : (bool) $tarefa["feita"];
$st = $pdo->prepare("UPDATE tarefas SET titulo = ?, feita = ? WHERE id = ?");
$st->execute([$titulo, (int) $feita, $id]);
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
Etapa 5 · Erros inesperados
Erros que nós prevemos já viram JSON (400, 404, 405, 422). Mas e um erro no nosso código, como um nome de tabela digitado errado (tarefa em vez de tarefas)?
$ curl -si localhost:8000/api.php/tarefas/1
HTTP/1.1 200 OK
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:08:39 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-type: text/html; charset=UTF-8
<br />
<b>Fatal error</b>: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 no such table: tarefa in /home/ana/api/api.php:46
Stack trace:
#0 /home/ana/api/api.php(46): PDO->prepare('SELECT * FROM t...')
#1 /home/ana/api/api.php(88): buscar(Object(PDO), 1)
#2 {main}
thrown in <b>/home/ana/api/api.php</b> on line <b>46</b><br />
Três problemas: o cliente recebe HTML em vez de JSON, o status diz 200 (sucesso!) e a mensagem revela detalhes internos (caminhos de arquivos, o SQL, nomes de tabelas) que ajudam um atacante. O conserto é um "pega-tudo": set_exception_handler registra uma função que roda quando uma exceção não é capturada por nenhum try/catch (Exceções):
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage());
responder(500, ["erro" => "Erro interno no servidor"]);
});
| Trecho | O que faz |
|---|---|
function (Throwable $e) { … } | Uma função anônima (aula) que recebe a exceção. Throwable pega exceções e erros (TypeError, PDOException…). |
error_log($e->getMessage()); | Guarda a mensagem verdadeira no log do servidor, para você (no php -S, ela aparece no terminal dele). |
responder(500, …) | Para o cliente, só uma mensagem genérica, em JSON, com o status certo. |
$ curl -si localhost:8000/api.php/tarefas/1
HTTP/1.1 500 Internal Server Error
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:04:32 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Content-Type: application/json; charset=utf-8
{
"erro": "Erro interno no servidor"
}
E, no terminal do servidor, a mensagem real, só para você:
$ php -S localhost:8000 [Fri Oct 16 21:04:31 2026] PHP 8.5.8 Development Server (http://localhost:8000) started [Fri Oct 16 21:04:31 2026] 127.0.0.1:59032 Accepted [Fri Oct 16 21:04:32 2026] SQLSTATE[HY000]: General error: 1 no such table: tarefa [Fri Oct 16 21:04:32 2026] 127.0.0.1:59032 [500]: GET /api.php/tarefas/1 [Fri Oct 16 21:04:32 2026] 127.0.0.1:59032 Closing
Num servidor de verdade, além disso, display_errors fica desligado (Depurar), e nenhum erro vaza na resposta.
Missão (etapa 5): o buscar desta caixa tem o erro de digitação de propósito (FROM tarefa). Não conserte o SQL ainda: acrescente o set_exception_handler para que a resposta seja {"erro": "Erro interno no servidor"} com status 500. Depois, conserte o nome da tabela e veja a tarefa 1 aparecer.
<?php
// ---------- Funções de apoio ----------
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefa WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
// ---------- A requisição ----------
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$caminho = $_SERVER["PATH_INFO"] ?? "/";
// ---------- Rotas ----------
if ($caminho === "/tarefas") {
if ($metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
header("Allow: GET, POST");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "PUT") {
$tarefa = buscar($pdo, $id);
$dados = lerCorpo();
$titulo = array_key_exists("titulo", $dados) ? validarTitulo($dados["titulo"]) : $tarefa["titulo"];
$feita = array_key_exists("feita", $dados) ? (bool) $dados["feita"] : (bool) $tarefa["feita"];
$st = $pdo->prepare("UPDATE tarefas SET titulo = ?, feita = ? WHERE id = ?");
$st->execute([$titulo, (int) $feita, $id]);
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
<?php
$arquivo = __DIR__ . "/tarefas.sqlite";
$novo = !file_exists($arquivo);
$pdo = new PDO("sqlite:$arquivo");
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if ($novo) {
$pdo->exec("CREATE TABLE tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO tarefas (titulo, feita) VALUES ('Comprar pão', 1), ('Estudar PHP', 0)");
}
return $pdo;
<?php
// ---------- Funções de apoio ----------
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefa WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
// ---------- Erros inesperados viram 500 em JSON ----------
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage());
responder(500, ["erro" => "Erro interno no servidor"]);
});
// ---------- A requisição ----------
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$caminho = $_SERVER["PATH_INFO"] ?? "/";
// ---------- Rotas ----------
if ($caminho === "/tarefas") {
if ($metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
header("Allow: GET, POST");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "PUT") {
$tarefa = buscar($pdo, $id);
$dados = lerCorpo();
$titulo = array_key_exists("titulo", $dados) ? validarTitulo($dados["titulo"]) : $tarefa["titulo"];
$feita = array_key_exists("feita", $dados) ? (bool) $dados["feita"] : (bool) $tarefa["feita"];
$st = $pdo->prepare("UPDATE tarefas SET titulo = ?, feita = ? WHERE id = ?");
$st->execute([$titulo, (int) $feita, $id]);
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
O código completo
Dois arquivos na pasta ~/api. Para rodar: php -S localhost:8000 nessa pasta e os comandos curl desta página em outro terminal.
<?php
$arquivo = __DIR__ . "/tarefas.sqlite";
$novo = !file_exists($arquivo);
$pdo = new PDO("sqlite:$arquivo");
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if ($novo) {
$pdo->exec("CREATE TABLE tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO tarefas (titulo, feita) VALUES ('Comprar pão', 1), ('Estudar PHP', 0)");
}
return $pdo;
<?php
// ---------- Funções de apoio ----------
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
// ---------- Erros inesperados viram 500 em JSON ----------
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage());
responder(500, ["erro" => "Erro interno no servidor"]);
});
// ---------- A requisição ----------
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$caminho = $_SERVER["PATH_INFO"] ?? "/";
// ---------- Rotas ----------
if ($caminho === "/tarefas") {
if ($metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
header("Allow: GET, POST");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "PUT") {
$tarefa = buscar($pdo, $id);
$dados = lerCorpo();
$titulo = array_key_exists("titulo", $dados) ? validarTitulo($dados["titulo"]) : $tarefa["titulo"];
$feita = array_key_exists("feita", $dados) ? (bool) $dados["feita"] : (bool) $tarefa["feita"];
$st = $pdo->prepare("UPDATE tarefas SET titulo = ?, feita = ? WHERE id = ?");
$st->execute([$titulo, (int) $feita, $id]);
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
api.php no meio?
Sem configurar nada, o php -S procura um arquivo para cada endereço; o PATH_INFO nos deixa usar um arquivo só. Em produção, o Apache ou o nginx são configurados para mandar todos os endereços para um index.php (o "front controller"), e a API fica com endereços limpos como /tarefas/2. No php -S, o mesmo se faz passando um "roteador": php -S localhost:8000 api.php, e lendo o caminho de parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH). É um dos desafios.
Desafios extras
- Filtro.
GET /tarefas?feita=0lista só as pendentes,?feita=1só as feitas (missão abaixo). - PATCH. Aceite
PATCHcomo sinônimo do PUT (in_array($metodo, ["PUT", "PATCH"])) e atualize o cabeçalhoAllow. - Data de criação. Uma coluna
criada_em TEXTpreenchida comdate("c")no POST e devolvida no JSON (Datas). - Paginação.
?pagina=2com 10 por página:LIMIT ? OFFSET ?no SQL (como no projeto do blog). - Senha da API. Exija um cabeçalho
X-Token($_SERVER["HTTP_X_TOKEN"]) igual a um valor guardado emgetenv("API_TOKEN"); sem ele,401. Compare comhash_equals. - Endereços limpos. Rode com
php -S localhost:8000 api.phpe troquePATH_INFOporparse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH), para atender/tarefas/2. - Um cliente. Uma página HTML com JavaScript (
fetch) ou um script PHP que use a API. Veja Consumir APIs.
Missão final: a caixa pede GET /api.php/tarefas?feita=0, mas a API devolve todas as tarefas. Quando $_GET["feita"] existir, liste só as tarefas com aquele valor (prepared statement com WHERE feita = ? e (int) $_GET["feita"]). Teste também ?feita=1 e sem filtro.
<?php
// ---------- Funções de apoio ----------
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
// ---------- Erros inesperados viram 500 em JSON ----------
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage());
responder(500, ["erro" => "Erro interno no servidor"]);
});
// ---------- A requisição ----------
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$caminho = $_SERVER["PATH_INFO"] ?? "/";
// ---------- Rotas ----------
if ($caminho === "/tarefas") {
if ($metodo === "GET") {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
responder(200, array_map("formatar", $tarefas));
}
if ($metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
header("Allow: GET, POST");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "PUT") {
$tarefa = buscar($pdo, $id);
$dados = lerCorpo();
$titulo = array_key_exists("titulo", $dados) ? validarTitulo($dados["titulo"]) : $tarefa["titulo"];
$feita = array_key_exists("feita", $dados) ? (bool) $dados["feita"] : (bool) $tarefa["feita"];
$st = $pdo->prepare("UPDATE tarefas SET titulo = ?, feita = ? WHERE id = ?");
$st->execute([$titulo, (int) $feita, $id]);
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
<?php
$arquivo = __DIR__ . "/tarefas.sqlite";
$novo = !file_exists($arquivo);
$pdo = new PDO("sqlite:$arquivo");
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if ($novo) {
$pdo->exec("CREATE TABLE tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO tarefas (titulo, feita) VALUES ('Comprar pão', 1), ('Estudar PHP', 0)");
}
return $pdo;
<?php
// ---------- Funções de apoio ----------
function responder(int $status, mixed $dados = null): never
{
http_response_code($status);
if ($dados !== null) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode($dados, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
exit;
}
function lerCorpo(): array
{
$tipo = $_SERVER["CONTENT_TYPE"] ?? "";
if (str_starts_with($tipo, "application/json")) {
$dados = json_decode(file_get_contents("php://input"), true);
if (!is_array($dados)) {
responder(400, ["erro" => "JSON inválido"]);
}
return $dados;
}
return $_POST;
}
function validarTitulo(mixed $titulo): string
{
if (!is_string($titulo) || trim($titulo) === "") {
responder(422, ["erro" => "O título é obrigatório"]);
}
$titulo = trim($titulo);
if (mb_strlen($titulo) > 100) {
responder(422, ["erro" => "O título pode ter no máximo 100 caracteres"]);
}
return $titulo;
}
function formatar(array $t): array
{
return ["id" => $t["id"], "titulo" => $t["titulo"], "feita" => (bool) $t["feita"]];
}
function buscar(PDO $pdo, int $id): array
{
$st = $pdo->prepare("SELECT * FROM tarefas WHERE id = ?");
$st->execute([$id]);
$tarefa = $st->fetch();
if ($tarefa === false) {
responder(404, ["erro" => "Tarefa $id não encontrada"]);
}
return $tarefa;
}
// ---------- Erros inesperados viram 500 em JSON ----------
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage());
responder(500, ["erro" => "Erro interno no servidor"]);
});
// ---------- A requisição ----------
$pdo = require __DIR__ . "/banco.php";
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo === "POST" && isset($_POST["_method"])) {
$metodo = strtoupper($_POST["_method"]); // formulários HTML só enviam GET e POST
}
$caminho = $_SERVER["PATH_INFO"] ?? "/";
// ---------- Rotas ----------
if ($caminho === "/tarefas") {
if ($metodo === "GET") {
if (isset($_GET["feita"])) {
$st = $pdo->prepare("SELECT * FROM tarefas WHERE feita = ? ORDER BY id");
$st->execute([(int) $_GET["feita"]]);
$tarefas = $st->fetchAll();
} else {
$tarefas = $pdo->query("SELECT * FROM tarefas ORDER BY id")->fetchAll();
}
responder(200, array_map("formatar", $tarefas));
}
if ($metodo === "POST") {
$dados = lerCorpo();
$titulo = validarTitulo($dados["titulo"] ?? null);
$st = $pdo->prepare("INSERT INTO tarefas (titulo) VALUES (?)");
$st->execute([$titulo]);
$id = (int) $pdo->lastInsertId();
header("Location: /api.php/tarefas/$id");
responder(201, formatar(buscar($pdo, $id)));
}
header("Allow: GET, POST");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
if (preg_match('#^/tarefas/(\d+)$#', $caminho, $partes)) {
$id = (int) $partes[1];
if ($metodo === "GET") {
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "PUT") {
$tarefa = buscar($pdo, $id);
$dados = lerCorpo();
$titulo = array_key_exists("titulo", $dados) ? validarTitulo($dados["titulo"]) : $tarefa["titulo"];
$feita = array_key_exists("feita", $dados) ? (bool) $dados["feita"] : (bool) $tarefa["feita"];
$st = $pdo->prepare("UPDATE tarefas SET titulo = ?, feita = ? WHERE id = ?");
$st->execute([$titulo, (int) $feita, $id]);
responder(200, formatar(buscar($pdo, $id)));
}
if ($metodo === "DELETE") {
buscar($pdo, $id);
$st = $pdo->prepare("DELETE FROM tarefas WHERE id = ?");
$st->execute([$id]);
responder(204);
}
header("Allow: GET, PUT, DELETE");
responder(405, ["erro" => "Método $metodo não permitido em $caminho"]);
}
responder(404, ["erro" => "Rota não encontrada: $metodo $caminho"]);
$ curl -s 'localhost:8000/api.php/tarefas?feita=0'
[
{
"id": 2,
"titulo": "Estudar PHP",
"feita": false
}
]
$ curl -s 'localhost:8000/api.php/tarefas?feita=1'
[
{
"id": 1,
"titulo": "Comprar pão",
"feita": true
}
]
Exercícios
1. Qual o método e o código de status certos para criar uma tarefa?
2. O cliente manda Content-Type: application/json com {"titulo": "Ler"}. Onde o PHP coloca esse corpo?
3. O título chegou vazio. Qual código a API responde?
4. Por que o set_exception_handler responde só "Erro interno no servidor" e não a mensagem da exceção?
Complete para definir o código de status da resposta:
Complete a opção do json_encode que mantém os acentos:
Resumo
- Uma API responde dados:
header("Content-Type: application/json; charset=utf-8")+json_encode(comJSON_UNESCAPED_UNICODE) +http_response_code. - A rota é o método (
REQUEST_METHOD) + o caminho (PATH_INFO);preg_matchextrai o id. - GET lê, POST cria (201 +
Location), PUT altera (200), DELETE apaga (204). Erros do cliente: 400, 404, 405, 422; nossos: 500. - Corpo JSON não vai para
$_POST:json_decode(file_get_contents("php://input"), true). Valide tudo. - Banco sempre com prepared statements;
fetch()pode darfalse. set_exception_handlertransforma qualquer erro inesperado num 500 em JSON, e a mensagem real vai para o log.
O próximo passo natural é escrever um programa que usa uma API: veja Consumir APIs. Para consultar códigos e funções, use a Referência.