JSON responses
Formato de cada ação, status codes, projeção de campos, transformers.
Toda ação de ApiResourceController devolve um Illuminate\Http\JsonResponse
via response()->json(...) — sem echo, sem view, sem MadResponse (esse
é o builder de ops parciais do MadWire, da camada reativa do admin; aqui a resposta é o
corpo JSON inteiro, de uma vez).
Listagem — GET /recurso
Params aceitos (query ou body JSON): filters, sort, direction (asc/desc), page, per_page.
200 OK
{
"data": [
{ "id": 1, "title": "Clientes", "groups_count": 2, "users_count": 1 }
],
"meta": {
"total": 1,
"per_page": 15,
"current_page": 1,
"last_page": 1
}
}
Um registro — GET /recurso/{id}
Corpo = o item, com a projeção de $showFields (ou toArray() completo se vazio), $appends e detalhes eager-loaded.
200 OK
{ "id": 1, "title": "Clientes", "groups": [...], "users": [...] }
404 Not Found
{ "error": "Resource not found" }
Criar / atualizar — POST · PUT/PATCH
Payload = master + uma chave por relação declarada em $details (array de linhas).
Resposta = o master recarregado com o eager-load default (não é o array que
você enviou — é o que ficou gravado, já com IDs gerados e relações atualizadas):
POST /api/import-templates → 201 Created
{
"id": 42,
"title": "Clientes",
"groups": [ { "id": 1, "group_id": 3 } ],
"users": [ { "id": 1, "user_id": 7 } ]
}
PUT /api/import-templates/42 → 200 OK
{ "id": 42, "title": "Clientes (renomeado)", "groups": [...], "users": [...] }
Validação falha → Laravel lança ValidationException, renderizada pelo handler
nativo como 422 com o shape padrão {message, errors} — o mesmo
formato que qualquer formulário Laravel produz:
422 Unprocessable Content
{
"message": "The title field is required.",
"errors": {
"title": ["The title field is required."],
"mapping_json": ["The mapping json field is required."]
}
}
Apagar — DELETE /recurso/{id}
Apaga master + detalhes em cascata (uma linha por relação de $details), em transação:
200 OK
{ "message": "Resource deleted successfully" }
404 Not Found
{ "error": "Resource not found" }
Projeção e alias — $indexFields / $showFields
Vazio → resposta = toArray() (tudo menos $hidden, mais $appends
e detalhes carregados). Com uma lista, suporta dot-notation de relação (achatada para
relacao_campo) e alias por chave:
protected array $indexFields = [
'id',
'total',
'cliente' => 'customer.name', // alias → chave 'cliente' na resposta
'customer.email', // achata → chave 'customer_email'
];
A PK do recurso sempre aparece na resposta (mesmo fora da lista), a menos que esteja explicitamente em $hidden.
Transformers
Pós-processam um campo já serializado — útil para formatar valores na borda sem mexer no model:
// dentro do controller (construtor, boot, ou onde fizer sentido)
$this->addTransformer('price', fn ($v, $model) => 'R$ ' . number_format($v, 2, ',', '.'));
Status codes usados
| Code | Quando |
|---|---|
200 | index, show, update, destroy bem-sucedidos. |
201 | store bem-sucedido. |
401 | mad.auth negou (sem sessão), ou mad.api sem Bearer / com token inválido, expirado ou revogado (resposta {"error": "…"} + header WWW-Authenticate: Bearer) — ver Autenticação. |
403 | mad.api: escopo do token inválido na revalidação (user/unit/tenant inexistente ou inativo, unit fora do usuário, unit sem empresa em modo de isolamento) ou token sem a ability exigida pela rota. Também o que o authorize() do controller costuma lançar. |
404 | show/update/destroy com PK inexistente, ou rota sem match dentro de /api/*. |
422 | ValidationException — master ou alguma linha de detalhe falhou Model::rules(). |
429 | mad.api: brute-force por IP (20 falhas de auth em 600s) — corpo {"error": "Too many requests. Try again later."} + Retry-After: 600. |
500 | Exceção não tratada — também renderizada como JSON (ver callout abaixo), nunca página de erro HTML. |
/api/* nunca devolve HTML
bootstrap/app.php tem
$exceptions->shouldRenderJsonWhen(fn ($request) => $request->is('api/*')).
Isso vale para QUALQUER exceção que escape do controller — não só as que o
ApiResourceController trata explicitamente (404/422). Um erro de SQL, um
TypeError, uma LogicException do seu código: tudo vira JSON
({"message": "..."}, com stack trace se APP_DEBUG=true),
consistente com o resto do contrato desta API.
Próximos passos
- Request injection — como os params de filtro/sort/payload chegam no controller.
- Exemplos práticos — as respostas acima, geradas de verdade via curl.