API HTTP: guia completo
Do primeiro endpoint à autenticação Bearer, validação, Resources, persistência, erros e testes.
Este guia parte do que está realmente implementado no projeto. Ele separa os endpoints já ativos dos exemplos que você criará ao acompanhar o tutorial; nenhum exemplo novo passa a existir até os arquivos e rotas serem adicionados.
/api/v1. Para o PHP consumir um serviço externo, use ApiClient e ApiRequest; são fluxos diferentes.1. O que é a API e como ela funciona
Uma API HTTP é um contrato entre cliente e servidor: o cliente envia método, caminho, cabeçalhos e, quando necessário, um corpo; o servidor valida a entrada e responde com status HTTP e representação dos dados. Na API própria deste MVC, a representação normal é JSON e a versão ativa está sob /api/v1.
O caminho real de uma chamada é: public/index.php → bootstrap/app.php → Framework\Core\Application::run() → inclusão de routes/web.php e de cada routes/api*.php → Router::dispatch() → middlewares da rota/grupo → Controller → Request/FormRequest/Validator → Model/Repository/Service → Resource → ApiResponse. O bootstrap também carrega Composer, .env, sessão e cabeçalhos globais; a autenticação Bearer da API, porém, é verificada pelo middleware próprio e não depende do cookie de login Web.
O arquivo routes/api.php registra hoje estas rotas ativas (os comentários de CRUD futuro não são endpoints):
| Método e caminho | Acesso | Comportamento atual |
|---|---|---|
POST /api/v1/auth/login | Público; limite adicional de login | Valida e-mail/senha, emite token Bearer e devolve UserResource. |
POST /api/v1/auth/logout | Bearer válido | Revoga o token da requisição. |
GET /api/v1/auth/me | Bearer válido | Devolve o usuário corrente. |
GET /api/v1/users | Bearer válido | Lista paginada; page e per_page vêm da query string. |
GET /api/v1/users/{id} | Bearer válido | Devolve UserResource ou JSON 404. |
OPTIONS para os caminhos declarados | CORS | Preflight recebe 204 no CorsMiddleware antes da action. |
Métodos e parâmetros de rota
O Router disponibiliza GET, POST, PUT, PATCH, DELETE, OPTIONS e match(). Placeholders como /users/{id} são passados por nome à action; parâmetros tipados escalares podem ser convertidos via Reflection. O override via _method só é aplicado quando a requisição original é POST e o valor é PUT/PATCH/DELETE; é uma conveniência para forms, não substituto necessário para clientes HTTP. Router::resource() gera sete actions convencionais, inclusive create/edit; ele não cria JSON Resources nem autenticação, então para API prefira registrar os verbos necessários explicitamente.
Todas as rotas ficam no grupo /api/v1 com CorsMiddleware e RateLimitMiddleware:api. As rotas privadas acrescentam ApiAuthMiddleware; login acrescenta o perfil login. A API não usa views para as respostas normais nem registra CsrfMiddleware no grupo; a segurança de escrita vem do Bearer e de autorização explícita. O bootstrap ainda inicia a sessão globalmente. É o mesmo Router, Request, Controllers, Models e banco do restante do projeto.
2. Arquivos: onde cada peça fica
| Caminho no projeto | Responsabilidade |
|---|---|
public/index.php | Front controller; recebe a requisição e chama o bootstrap. |
bootstrap/app.php | Carrega Composer/.env, configura caminhos e instancia Application. |
routes/api.php | Registra prefixo, verbos, Controllers e middlewares. A Application também carrega routes/api*.php. |
config/api.php + .env | CORS e configuração de integrações; CORS_ALLOWED_ORIGINS alimenta as origens aceitas. |
app/Http/Controllers/Api/ | Actions HTTP de API; ApiController é a base da aplicação, sobre Controller do Framework. |
app/Http/Requests/ e app/Http/Requests/Api/ | FormRequests compartilhados ou específicos da API. A classe base é App\Http\Requests\FormRequest. |
app/Http/Middlewares/ | Bearer auth, CORS e rate limit pertencem à aplicação de exemplo. |
app/Http/Resources/ | Transformação explícita Model → array JSON; por exemplo, UserResource. |
app/Models/, app/Repositories/, app/Services/ | Modelo/persistência, consultas de domínio e regras de aplicação; use apenas as camadas necessárias. |
framework/Http/Request.php, Router.php, framework/Http/Api/ApiResponse.php | Infraestrutura real de entrada, despacho e envelope JSON. |
database/migrations/ | Schema da aplicação; usuários e api_tokens já têm migrations no projeto. |
Pré-requisitos e configuração
Parta da raiz do projeto. Instale dependências, copie e edite .env, configure MySQL e crie o banco vazio com as credenciais corretas antes de migrar (migrate cria tabelas; não cria o database MySQL). As rotas públicas de demonstração sem banco podem responder sem consulta, mas login e endpoints de usuários dependem das tabelas e das migrations.
# Na raiz do projeto; preencha antes .env com dados locais de MySQL
composer install
cp .env.example .env
# Edite .env: APP_URL, APP_ENV, APP_DEBUG e DB_HOST/PORT/DATABASE/USERNAME/PASSWORD
php mvc key:generate
php mvc migrate
# Opcional: somente em banco local descartável/de desenvolvimento
php mvc seed:run UserSeeder
php mvc serve
php mvc serve inicia o servidor local na porta 8000. O document root de produção deve ser public/; o .htaccess usa mod_rewrite para encaminhar caminhos inexistentes ao front controller. O comando de servidor é para desenvolvimento, não para produção. A aplicação precisa de vendor/autoload.php; se não houver dependências instaladas, o bootstrap responde 500.
Para permitir uma SPA em outra origem, configure por exemplo CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com. O wildcard * só é aceito fora de APP_ENV=production; CORS só governa leitura pelo navegador e não autentica nem autoriza clientes. Em caminhos novos, adicione também uma rota OPTIONS explícita quando o navegador for fazer preflight (por exemplo, por usar Authorization ou Content-Type: application/json); o Router precisa casar o caminho OPTIONS para o CorsMiddleware poder encerrá-lo com 204.
3. Iniciante — criar seu primeiro endpoint do zero
Cenário e objetivo
Você quer confirmar que a aplicação inicia e que uma rota API devolve JSON, sem banco, Model ou autenticação. O endpoint será público e não exporá dados privados. Ele será acrescentado ao grupo existente em routes/api.php, que já aplica CORS e rate limit.
Etapa 1 — crie o Controller
Crie app/Http/Controllers/Api/HealthApiController.php. Herdar de ApiController dá acesso aos helpers de envelope e à Request herdada.
<?php
namespace App\Http\Controllers\Api;
final class HealthApiController extends ApiController
{
public function show(): void
{
$this->success(
['api' => 'v1', 'status' => 'ok'],
'API disponível.'
);
}
}
O método chama success(data, message). Esse helper envia status 200, Content-Type: application/json e o envelope padronizado; é terminal e encerra a requisição.
Etapa 2 — registre a rota
Em routes/api.php, importe a classe e acrescente a action dentro do closure já existente do grupo /api/v1; não crie um segundo prefixo acidentalmente.
<?php
use App\Http\Controllers\Api\HealthApiController;
// Dentro do group /api/v1 já existente em routes/api.php:
$r->get('/health', [HealthApiController::class, 'show']);
O Router combina o prefixo do grupo com /health, portanto o caminho final é /api/v1/health. Se a action não existir ou o middleware estiver escrito errado, isso vira falha de servidor; confira a grafia e os namespaces.
Etapa 3 — execute e confira
Em outro terminal, rode o servidor local. Depois da chamada, espere HTTP 200, Content-Type JSON e os campos de envelope a seguir:
curl -i http://localhost:8000/api/v1/health \
-H 'Accept: application/json'
{
"success": true,
"message": "API disponível.",
"data": {"api": "v1", "status": "ok"}
}
Se receber 404, confirme se salvou o arquivo na pasta certa, se adicionou a rota ao grupo real e se acessou o método/caminho corretos. Se receber 500 por autoload, rode composer install e confira o log PHP.
4. Entradas HTTP e validação
A classe Framework\Http\Request encapsula dados globais, mas os métodos não juntam todas as fontes:
| Origem | Como ler | Detalhe que evita erro |
|---|---|---|
Parâmetro de caminho /users/{id} | Argumento tipado da action: show(int $id) | Router mapeia por nome e faz cast escalar; cast para int não valida que a entrada original era um inteiro positivo. |
Query string ?page=2 | $this->request->get('page', 1) | get() lê $_GET e sanitiza texto; valide/faça cast e limite valores. |
| Form-urlencoded / form POST | post(), all(), raw() ou FormRequest | all() lê somente $_POST, não a query nem JSON. |
| JSON no body | $this->request->json() ou um FormRequest com Content-Type JSON | input(), post() e all() não leem o JSON. O JSON malformado vira array vazio em Request::json(); não espere exceção JSON automática. |
| Headers | header('Accept'), header('Authorization') | Request monta o índice HTTP_* em $_SERVER; para detectar corpo JSON use isJson() (que lê CONTENT_TYPE), não dependa de header('Content-Type'). |
| Arquivo enviado | file('foto')/hasFile('foto') e Validator/Upload | FormRequest inclui o nome original no input, mas não valida o conteúdo binário do upload por si só. |
Use cabeçalhos coerentes: Accept: application/json informa o formato esperado ao cliente; Content-Type: application/json descreve o corpo JSON enviado. Os helpers ApiResponse sempre respondem JSON, independentemente de Accept. JSON de entrada deve ser objeto/campos, pois FormRequest converte o corpo em array associativo.
Cenário e objetivo
Agora o endpoint recebe nome no corpo JSON. Queremos rejeitar campo ausente ou curto antes de montar a resposta. Crie app/Http/Requests/Api/GreetingRequest.php e app/Http/Controllers/Api/GreetingApiController.php; use o helper de validação da base API.
<?php
namespace App\Http\Requests\Api;
use App\Http\Requests\FormRequest;
final class GreetingRequest extends FormRequest
{
public function authorize(): bool
{
return true; // endpoint didático público; remova/ajuste em produção
}
public function rules(): array
{
return ['name' => 'required|min:2|max:80'];
}
public function messages(): array
{
return ['name.required' => 'Informe o nome.'];
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Api\GreetingRequest;
final class GreetingApiController extends ApiController
{
public function store(GreetingRequest $request): void
{
$data = $this->validated($request); // encerra em JSON 422 se inválido
$this->success(
['greeting' => 'Olá, ' . $data['name'] . '!'],
'Saudação criada.'
);
}
}
O Router reconhece argumento de action cuja classe estende App\Http\Requests\FormRequest, instancia-o e passa os parâmetros de rota ao construtor. A validação é calculada no construtor, mas não envia automaticamente 422 para API: chame $this->validated($request). Esse helper devolve apenas dados que passaram nas regras ou encerra com envelope JSON 422. Consulte regras de validação e Request/FormRequest.
Registre também o verbo e preflight dentro do grupo:
<?php
use App\Http\Controllers\Api\GreetingApiController;
use App\Http\Controllers\Api\PreflightController;
// Dentro do group /api/v1 já existente; registre OPTIONS para o preflight do browser.
$r->options('/greetings', [PreflightController::class, 'handle']);
$r->post('/greetings', [GreetingApiController::class, 'store']);
Teste entrada válida e inválida. Com nome válido, espera-se 200; com nome ausente ou curto, espera-se 422, success:false e mapa de erros:
curl -i -X POST http://localhost:8000/api/v1/greetings \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"name":"Ana"}'
{
"success": true,
"message": "Saudação criada.",
"data": {"greeting": "Olá, Ana!"}
}
{
"success": false,
"message": "Os dados enviados são inválidos.",
"errors": {"name": ["Informe o nome."]}
}
Para um endpoint que precise ler query strings, use Request::get() e passe os valores pelo Validator ou aplique validação explícita; o FormRequest desta implementação captura JSON ou POST, não mescla automaticamente $_GET.
5. Intermediário — autenticação Bearer e leitura paginada
Cenário e objetivo
O projeto já traz login API, tokens, consulta do usuário autenticado e listagem/leitura de usuários. Esse fluxo demonstra Request → FormRequest → Controller → Repository → Resource → ApiResponse. Não há escrita de usuário ativa antes de adicionar a extensão avançada abaixo.
Faça login e receba o token
POST /api/v1/auth/login aceita JSON, reutiliza LoginRequest, autentica via User::authenticate() e emite token com ApiTokenRepository. Em desenvolvimento, o seeder fornece contas de demonstração; leia o arquivo database/seeds/UserSeeder.php e nunca rode contas/senhas de demonstração em produção.
curl -i -X POST http://localhost:8000/api/v1/auth/login \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"SENHA_LOCAL"}'
Resposta representativa (valores de ID/data variam); copie o campo data.token para o cliente com cuidado. O valor puro é exibido uma única vez:
{
"success": true,
"message": "Login realizado com sucesso.",
"data": {
"token": "<TOKEN-EXIBIDO-UMA-VEZ>",
"token_type": "Bearer",
"user": {
"id": 1,
"name": "Administrador",
"email": "admin@example.com",
"role": "admin",
"active": true,
"created_at": "2026-09-28 10:00:00"
}
}
}
O Model gera token aleatório de 64 caracteres hexadecimais e grava apenas SHA-256 na tabela api_tokens; a migration cria chave única para hash e relação com users. O middleware procura token válido, exige usuário ativo, preenche ApiAuthContext e atualiza last_used_at. Envie o token em Authorization: Bearer …, não em query string. Logout revoga o registro atual; não há endpoint de refresh/revogação de todos os dispositivos exposto nas rotas.
expires_at, mas o endpoint de login chama issue($userId, 'api-login') sem data de expiração. Portanto, os tokens emitidos por esse fluxo não expiram pelo campo expires_at; permanecem válidos até serem revogados ou o usuário ser desativado. Não assuma que existe rotação ou refresh automático.Consuma a lista autenticada
UserApiController::index() lê page e per_page pela query string; limita page ao mínimo 1 e per_page entre 1 e 100 (padrão 15). Ele chama UserRepository::paginate(), transforma cada objeto com UserResource::collection() e passa os metadados a paginated().
TOKEN='<cole o token recebido no login>'
curl -i 'http://localhost:8000/api/v1/users?page=1&per_page=15' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'
{
"success": true,
"message": "Dados encontrados.",
"data": [
{
"id": 1,
"name": "Administrador",
"email": "admin@example.com",
"role": "admin",
"active": true,
"created_at": "2026-09-28 10:00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 15,
"total": 1,
"last_page": 1,
"from": 1,
"to": 1
}
}
O formato acima ilustra uma página com um resultado. UserResource expõe explicitamente id, name, email, role, active e created_at; não inclui password nem remember_token. Com coleção vazia, o Model desta versão retorna total=0, last_page=0, from=1, to=0. O limite 100 é política deste Controller/gerador de API, não teto geral do Model.
Leia um registro e entenda o 404
GET /api/v1/users/{id} recebe int $id, consulta UserRepository::findById() e responde com Resource ou envelope JSON de erro 404. Já um caminho sem rota, por exemplo /api/v1/nao-existe, passa pelo handler genérico de 404 do Router, que atualmente imprime HTML. “API usa JSON” não significa que todo handler global negocia JSON.
{
"success": false,
"message": "Usuário não encontrado."
}
6. Avançado — extensão organizada de cadastro
users, User::createWithHash(), UserRepository::findByEmail(), UserResource e os helpers do Controller já existem.Cenário e objetivo
Um cliente autenticado precisa cadastrar um usuário comum. O body contém nome, e-mail e senha; não aceitamos role ou active vindos do cliente, para impedir autoelevação de privilégio. A senha precisa ser preservada durante sanitização e armazenada com hash pelo Model. O resultado usa 201 e Resource, nunca o objeto bruto do Model.
Passo A — crie e valide o FormRequest
O gerador php mvc make:api-request StoreUserApiRequest cria um stub sob app/Http/Requests/Api/; revise-o e complete autorização/regras. A action recebe o objeto tipado; o middleware da rota executa antes e preenche ApiAuthContext.
<?php
namespace App\Http\Requests\Api;
use App\Http\Requests\FormRequest;
use Framework\Http\Request;
use Framework\Http\Api\ApiAuthContext;
final class StoreUserApiRequest extends FormRequest
{
public function authorize(): bool
{
// A rota aplica ApiAuthMiddleware antes de construir este FormRequest.
return ApiAuthContext::check();
}
public function rules(): array
{
return [
'name' => 'required|min:2|max:150',
'email' => 'required|email|max:180',
'password' => 'required|min:8',
];
}
public function messages(): array
{
return [
'email.email' => 'Informe um e-mail válido.',
];
}
public function sanitize(): array
{
$clean = Request::sanitizeValue($this->input);
$clean['email'] = strtolower(trim((string) ($this->input['email'] ?? '')));
$clean['password'] = (string) ($this->input['password'] ?? '');
return $clean;
}
}
Passo B — exponha a escrita segura pelo Repository
User::createWithHash() já define hash bcrypt, role padrão member e active padrão 1. O Repository atual não tem esse wrapper, então acrescente explicitamente o método a app/Repositories/UserRepository.php:
<?php
namespace App\Repositories;
use Framework\Database\Repository;
use App\Models\User;
final class UserRepository extends Repository
{
protected string $modelClass = User::class;
public function findByEmail(string $email): object|false
{
return $this->model()->findBy('email', $email);
}
public function getActive(): array
{
return $this->model()->where('active', 1)->orderBy('name')->get();
}
public function search(string $term, int $page = 1, int $perPage = 15): array
{
$like = '%' . $term . '%';
return $this->model()
->select('id', 'name', 'email', 'role', 'active', 'created_at')
->where('name', $like, 'LIKE')
->orWhere('email', $like, 'LIKE')
->orderBy('name')
->paginate($perPage, $page);
}
// Novo wrapper para o método createWithHash() já existente em User.
public function createWithHash(array $data): string|false
{
/** @var User $model */
$model = $this->model();
return $model->createWithHash($data);
}
}
A regra do Request verifica formato e limites; o Service verifica duplicidade antes de gravar. A constraint UNIQUE existente em users.email é a proteção concorrente definitiva; o Service converte somente violação MySQL de entrada duplicada (SQLSTATE 23000/código 1062) em resultado de domínio, sem colocar detalhes SQL na resposta.
Passo C — crie o Service
Crie app/Services/Users/RegisterUserService.php. Service recebe dados já validados e não conhece HTTP, status, headers nem JSON. A injeção é explícita: o projeto não declara um container de dependências automático.
<?php
namespace App\Services\Users;
use App\Repositories\UserRepository;
use Framework\Core\Service;
use PDOException;
final class RegisterUserService extends Service
{
public function __construct(
private readonly UserRepository $users = new UserRepository()
) {}
/** @return array{created:bool,reason?:string,id?:int} */
public function register(array $data): array
{
if ($this->users->findByEmail($data['email'])) {
return ['created' => false, 'reason' => 'email_exists'];
}
try {
// Método novo do UserRepository, que delega a User::createWithHash().
$id = $this->users->createWithHash($data);
} catch (PDOException $e) {
// A constraint UNIQUE do banco cobre a corrida entre validação e INSERT.
if ((string) $e->getCode() === '23000' && (int) ($e->errorInfo[1] ?? 0) === 1062) {
return ['created' => false, 'reason' => 'email_exists'];
}
throw $e;
}
if ($id === false) {
return ['created' => false, 'reason' => 'persistence'];
}
return ['created' => true, 'id' => (int) $id];
}
}
Passo D — orquestre no Controller e devolva Resource
Adicione imports/propriedades e inicialização no Controller, preservando parent::__construct(); implemente a action. O snippet indica a action completa e as dependências que ela espera:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Api\StoreUserApiRequest;
use App\Services\Users\RegisterUserService;
use App\Http\Resources\UserResource;
use App\Repositories\UserRepository;
use Framework\Support\Logger;
final class UserRegistrationApiController extends ApiController
{
private UserRepository $users;
private RegisterUserService $registration;
public function __construct()
{
parent::__construct();
$this->users = new UserRepository();
$this->registration = new RegisterUserService($this->users);
}
public function store(StoreUserApiRequest $request): void
{
$data = $this->validated($request);
$result = $this->registration->register($data);
if (!$result['created'] && ($result['reason'] ?? '') === 'email_exists') {
$this->error('Este e-mail já está cadastrado.', [], 409);
}
if (!$result['created']) {
$this->error('Não foi possível criar o usuário.', [], 500);
}
$user = $this->users->findById($result['id']);
if (!$user) {
Logger::error('API: usuário criado, mas não pôde ser recarregado.', ['id' => $result['id']]);
$this->error('Não foi possível concluir a operação.', [], 500);
}
$this->success(UserResource::make($user), 'Usuário criado.', 201);
}
}
Com sucesso, a senha não aparece porque UserResource escolhe os campos de saída; o id recém-criado é relido pelo Repository para produzir o objeto usado pela Resource.
Passo E — registre POST e OPTIONS
Em routes/api.php, dentro do mesmo grupo, adicione POST protegido e OPTIONS para o preflight. O login já é público; este exemplo de cadastro exige um token existente. Como ApiAuthMiddleware apenas autentica, ele não aplica política de papel/admin; se o recurso exigir autorização mais restrita, escreva e registre essa política explicitamente.
<?php
use App\Http\Controllers\Api\UserRegistrationApiController;
// Dentro do group /api/v1 em routes/api.php.
// OPTIONS /users já está registrado na aplicação existente.
$r->post('/users', [UserRegistrationApiController::class, 'store'], ['ApiAuthMiddleware']);
Passo F — teste o contrato
Envie os três campos permitidos, com um token obtido no login:
curl -i -X POST http://localhost:8000/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"name":"Joana","email":"joana@example.com","password":"UmaSenha-Forte-123"}'
O resultado esperado é HTTP 201 e um Resource sem senha:
{
"success": true,
"message": "Usuário criado.",
"data": {
"id": 42,
"name": "Joana",
"email": "joana@example.com",
"role": "member",
"active": true,
"created_at": "2026-09-28 10:15:00"
}
}
Teste também: token ausente (401), token inválido/expirado (401), e-mail repetido (409 pelo Service), campo ausente ou e-mail inválido (422) e ID de consulta inexistente (404). Para qualquer erro inesperado, o comportamento genérico atual pode ser HTML 500; veja tratamento de falhas. Não esconda a falha de banco com um 201.
Quando acrescentar Model/migration
O exemplo reaproveita a tabela existente, portanto não cria Model nem migration. Para uma entidade nova, o fluxo é: php mvc make:migration e editar a migration PHP; php mvc make:model e conferir tabela/fillable; php mvc make:repository e declarar $modelClass; opcionalmente Service/FormRequest/Resource/Controller; registrar verbos explícitos em routes/api.php; rodar php mvc migrate. Gerador de Model não cria migration executável automaticamente e gerador de Controller não ativa rotas.
7. Middleware, CORS e controle de acesso
- ApiAuthMiddleware: exige o prefixo
Bearer, valida o hash do token, usuário e estado ativo; erros de autenticação são JSON 401. Não confundir comAuthMiddlewareWeb, baseado em sessão. - ApiAuthContext: fornece
check(),user(),id()etoken()para a requisição atual. Em API, use esse contexto, nãoAuth::check()ouController::user(), que leem autenticação de sessão. - CorsMiddleware: lê
config/api.php; permite origens exatas configuradas e responde OPTIONS com 204. Origem não autorizada não recebeAccess-Control-Allow-Origin. CORS não impede chamadas server-to-server e não substitui token/autorização. - RateLimitMiddleware: perfil
apipermite 60 chamadas/minuto por IP; perfilloginpermite 5/15 min e é aplicado adicionalmente ao limite do grupo. O armazenamento é em arquivos emstorage/cache/ratelimit; não é Redis nem limitador distribuído.
Detalhe importante: o código do rate limiter escolhe JSON pelo Content-Type: application/json (Request::isJson()) ou por X-Requested-With, não apenas por Accept. Assim, uma requisição GET que ultrapasse o limite e envie somente Accept: application/json pode receber o fluxo HTML de redirect atual. Verifique esse cenário no deployment se o contrato exigir que todo 429 de API seja JSON. O perfil de login lê e-mail de $_POST ao montar a chave; em login JSON esse campo não está em $_POST e o bucket acaba sendo por IP com e-mail vazio nesta implementação.
8. Respostas, status HTTP e exceções
App\Http\Controllers\Api\ApiController expõe success(data,message,status,meta), error(message,errors,status), paginated(result,message) e validated(FormRequest). Eles delegam a Framework\Http\Api\ApiResponse, que seta Content-Type/status, serializa JSON e chama exit. Sucesso tem success, message, data e meta opcional; erro tem success:false, message e errors apenas quando não vazio.
| Status | Quando aparece neste código | Formato/observação |
|---|---|---|
| 200 | Sucesso padrão de ApiResponse; login, logout, me e leitura usam o default. | JSON envelope. |
| 201 | Suportado por argumento de status de success(); usado na extensão avançada, não nas rotas ativas originais. | JSON envelope de sucesso. |
| 204 | Preflight OPTIONS encerrado pelo CorsMiddleware. | Sem corpo. |
| 400 | Status padrão de error() quando não se informa outro. | JSON quando chamada pelo helper API. |
| 401 | Bearer ausente/inválido, credencial inválida ou expirada. | JSON da API. Usuário inativo também tende a 401 porque User::authenticate() o rejeita antes da branch 403 do controller de login. |
| 403 | FormRequest com authorize() === false. | JSON se Accept ou Content-Type forem JSON; este caminho envia payload simples sem ApiResponse. |
| 404 | UserApiController::show() para ID sem registro. | JSON. Rota/path sem correspondência usa handler genérico Router e devolve HTML 404. |
| 405 | URI existe, mas verbo não está registrado. | O Router envia Allow e JSON em caminho /api/; endpoint POST /users ainda não existe antes do exemplo avançado. |
| 409 | Conflito da extensão de cadastro (e-mail duplicado). | JSON quando o Controller chama error com status 409. |
| 422 | FormRequest inválido via ApiController::validated(). | JSON com mapa de mensagens por campo. |
| 429 | Limite de requisições atingido. | Retry-After; JSON/redirect depende dos checks de AJAX/Content-Type citados acima. |
| 500 | Falha não tratada durante middleware/action ou serialização. | ApiResponse falha ao codificar → JSON 500. Throwable capturado pelo Router usa view/fallback HTML 500; não existe handler global que normalize toda exceção em JSON. |
| 419 / 503 | Constam no mapa de mensagens de erro da Application. | Não são um contrato geral de endpoints API ativos; 419 é ligado a CSRF Web e a API não registra CsrfMiddleware. |
Para manter JSON também em falhas conhecidas, trate erros esperados na action/Service e chame $this->error() com status adequado; registre contexto técnico com Logger, mas não retorne SQL, stack trace, arquivo, token ou senha ao consumidor. Exceções desconhecidas não são automaticamente convertidas em envelope API: Router captura falha da rota e usa view/fallback de erro 500. O abort() do Controller base não é substituto de ApiController::error() para este contrato. Não há uma classe genérica Framework\Http\Response.
9. Como testar, consumir e manter
Teste do endpoint e token
Use cURL com -i para conferir status e headers. Primeiro faça login, depois substitua $TOKEN pelo valor de data.token:
curl -i -X POST http://localhost:8000/api/v1/auth/login \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"SENHA_LOCAL"}'
TOKEN='<cole o token recebido no login>'
curl -i 'http://localhost:8000/api/v1/users?page=1&per_page=15' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'
Para verificar OPTIONS no browser, configure origem no .env e envie o preflight:
curl -i -X OPTIONS http://localhost:8000/api/v1/users \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: authorization,content-type'
Confirme 204, Access-Control-Allow-Origin somente para origem permitida e headers/métodos declarados. cURL não aplica política CORS; a restrição é executada por browsers.
Matriz mínima antes de publicar
| Caso | Esperado no código/contrato |
|---|---|
| Rota pública de health / JSON válido | 200, Content-Type JSON e envelope estável. |
| Login com senha inválida | 401; não revelar se e-mail ou senha isoladamente estava errada. |
| Login com campo ausente | 422 e errors por campo. |
| Sem Authorization em endpoint privado | 401. |
| Token revogado no logout | Próxima chamada protegida com esse token: 401. |
| ID válido no formato, mas inexistente | 404 JSON no GET /users/{id}. |
| Verbo não registrado | 405; verificar header Allow e corpo. |
| Origem CORS aceita/rejeitada + OPTIONS | 204 no preflight e cabeçalho ACAO só para origem aceita. |
| Limite de chamadas | 429 e Retry-After; conferir o formato JSON/HTML conforme os cabeçalhos usados. |
| Erro inesperado de Controller | 500; confirmar que não vaza detalhes e notar que fallback pode ser HTML. |
O projeto declara PHPUnit e configura tests/Unit/tests/Feature com o script composer test, mas não entrega uma suíte de testes HTTP API ou cliente de testes pronto nessas pastas. Adicione seus próprios testes com banco de teste isolado; não execute testes que apagam/migram um banco de produção. Os exemplos desta página podem ser testados manualmente via cURL no ambiente local.
Consumo de outra API a partir do MVC
Isso é fluxo de saída, não uma rota recebida. Encapsule-o em Integration/Gateway/Service e use Framework\Http\Api\ApiClient (cURL, timeout por instância, Bearer/basic auth, ApiException); não escreva cURL direto no Controller. A referência de construtor, retorno, timeout e erros está em ApiClient e ApiRequest. Não misture o token do cliente que chama sua API com credenciais usadas pelo servidor para chamar provedores externos.
10. O que evitar — e como fazer corretamente
| Evite | Problema | Faça assim |
|---|---|---|
| Tratar comentários de rota/gerador como endpoints ativos | Comentários não registram rota; stub pode conter método 501. | Confira routes/api.php e implemente Controller/middleware antes de anunciar o endpoint. |
Ler JSON por input()/all() | Esses helpers não mesclam body JSON. | Use FormRequest com Content-Type JSON ou json(); depois valide. |
| Usar sanitização como validação de domínio | Escape HTML não garante formato, faixa ou autorização e pode alterar senha. | Use Validator/FormRequest; personalize sanitize() para preservar senha como LoginRequest faz. |
Serializar Model diretamente ou confiar só em $hidden | Uma nova coluna pode vazar; hidden não é filtro universal de json_encode. | Exponha lista explícita em ApiResource. |
| Usar Auth Web para validar Bearer | Auth/AuthMiddleware trabalha com sessão/cookie e não verifica token API. | Registre ApiAuthMiddleware e use ApiAuthContext. |
| Confundir CORS com controle de acesso | Curl, apps nativos e servidores não são bloqueados por CORS. | Use autenticação/autorização real em cada rota protegida. |
Usar * CORS em produção ou pôr token na URL | A origem amplia superfície; query pode vazar em logs/histórico. | Allowlist por origem e header Authorization; proteja armazenamento do cliente. |
| Assumir expiração/refresh/revogação global de token | Login atual emite sem expires_at; não há endpoints de refresh/logout-all. | Revogue pelo fluxo disponível; implemente e teste expiração/rotação antes de depender dela. |
| Assumir que ApiAuthMiddleware checa papel | Ele valida token e usuário ativo; não aplica escopos ou papel. | Defina regra de autorização específica antes de permitir operações sensíveis. |
Usar Router::resource() como REST JSON pronto | Gera também create/edit e não cria Resource, validação, auth ou implementação; pode registrar actions HTML inexistentes. | Registre explicitamente os verbos API e aplique middleware/política. |
| Retornar 200 para toda operação | Clientes não distinguem criação, invalidação e falha. | Use 200/201/204/4xx adequados ao contrato; helpers API aceitam status explícito. |
| Esperar que toda exceção vire JSON | Fallback Router/Application é HTML e não mapeia todo erro de domínio em envelope. | Trate falhas esperadas no Controller/Service, logue internamente e responda com ApiController helpers. |
Confiar apenas em regra unique | Requisições concorrentes podem passar na validação ao mesmo tempo. | Mantenha UNIQUE no banco e traduza conflito conhecido sem expor SQL. |
Usar AuthService Web para emitir token API | Esse Service usa Auth de sessão; o fluxo Bearer existente é implementado em AuthApiController/ApiTokenRepository. | Separe contexto de sessão e token; reaproveite regras puras, não sessão como credencial de API. |
11. Recursos que não existem ou não são automáticos
- Não há OpenAPI/Swagger gerado nem catálogo que registre automaticamente rotas novas; esta página e a referência de classes são documentação manual.
- Não há injeção automática de Controllers/Services/Repositories. Router instancia Controller sem argumentos; construtores inicializam dependências explicitamente. Só FormRequest tipado na assinatura de action tem construção especial.
- Não há middleware pipeline com request/response encadeados; middleware é classe cujo
handle(Request $request, ?param)roda antes da action e pode encerrar a saída. - Não há CRUD de escrita de usuários ativo; no generator API, store/update podem estar marcados 501 até serem implementados.
POST/PUT/PATCH/DELETEsó existem depois de registrados. - Não há JWT, OAuth2, refresh token, scopes/permissões por token ou policy engine no middleware API entregue; há token aleatório pessoal com hash SHA-256 e contexto de usuário.
- Não há resposta JSON global para 404 de rota ou Throwable de ação; use helpers explícitos para contratos de API, e reveja os handlers HTML 404/500 se seu produto exigir JSON uniforme.
config/api.phptrata CORS e integrações de provedor; isso não configura automaticamente autenticação por token nem validação dos endpoints.
Checklist de publicação
- Confirme que cada rota está registrada, protegida e testada com verbo/caminho definitivos.
- Use HTTPS,
APP_DEBUG=falseem produção e nunca publique .env, senhas, tokens ou stack traces. - Configure origens CORS exatas e teste preflight; CORS não substitui autorização.
- Troque/remova as contas do UserSeeder; restrinja respostas via Resource e não aceite role/active sem política.
- Decida política de expiração/revogação de tokens: o login ativo não configura
expires_at. - Valide entrada, limite paginação, use constraints no banco e teste 401/403/404/405/409/422/429/500.
- Leia as páginas de roteamento, Request/FormRequest, respostas, middleware, Repository, Service e Model ao ampliar o domínio.
Rate limiting: comportamento durante falha de armazenamento
Os perfis api/default são fail-open: se o armazenamento em arquivo falhar e hit() retornar null, a requisição segue sem throttling. Perfis de login/register/forgot são fail-closed e podem bloquear quando não conseguem verificar o limite. Monitore permissões, espaço em disco, storage/cache/ratelimit e erros de I/O; o limite não é garantia durante falhas de armazenamento.
409 por conflito UNIQUE não é garantido em corrida
O exemplo de cadastro pode mapear para 409 uma duplicidade que foi detectada previamente pela consulta de domínio. Porém, se outra requisição inserir o mesmo valor entre a consulta e o INSERT, a violação UNIQUE no banco não é traduzida automaticamente para 409 pelo Service atual: ele não preserva necessariamente SQLSTATE/errorInfo de modo que a API possa distinguir a condição. Não capture indiscriminadamente PDOException no controller supondo que o framework a propaga; documente 409 concorrente apenas após uma alteração explícita que preserve e mapeie o erro do banco.