DEV Community

Denis Augusto
Denis Augusto

Posted on

API Resources no Laravel: pare de dar return no seu Model cru

Sua API tá contando segredo pro mundo inteiro

Você monta o endpoint, testa no Insomnia, o JSON volta bonitinho e você segue a vida. Deploy feito, tá funcionando.

Só que naquele JSON, junto com o name e o email que você queria mostrar, foi também o password (mesmo que hasheado), o remember_token, o stripe_id do cliente e aquele campo internal_notes que só o time devia ver.

Ninguém reparou. Até alguém reparar. 😬

Esse é o preço de retornar o Model direto. E a solução no Laravel tem nome: API Resources.

O código que todo mundo já escreveu

Olha esse controller. Aposto que você já fez algo parecido:

public function show(User $user)
{
    return response()->json($user);
}

// ou a versão "prática":
public function index()
{
    return User::all();
}
Enter fullscreen mode Exit fullscreen mode

Funciona. Retorna o usuário. Mas quando você faz return $user, o Laravel chama toArray() no Model e despeja todas as colunas que vieram do banco no JSON.

Toda coluna. A que você quer e a que você esqueceu que existia. Se amanhã alguém adicionar um campo documento ou salario na tabela, ele vai vazar de graça na sua API sem ninguém pedir.

Você não tem controle nenhum sobre o formato da resposta. E API é contrato — sair jogando o banco cru é o oposto de contrato.

O $hidden ajuda, mas não resolve

"Ah, mas eu uso $hidden no Model." Ótimo, é melhor que nada:

protected $hidden = ['password', 'remember_token'];
Enter fullscreen mode Exit fullscreen mode

O problema é que $hidden é uma blacklist. Você precisa lembrar de esconder cada campo sensível novo. Esqueceu um? Vazou.

E tem outro detalhe: o mesmo Model serve pra vários lugares. Às vezes você quer o email na resposta, às vezes não. O $hidden é global demais pra essa decisão, que na real é por endpoint.

A solução: uma camada só pra montar a resposta

API Resource é uma classe que fica entre o seu Model e o JSON. Ela decide, campo a campo, o que vira resposta. É uma whitelist: só sai o que você escreveu ali.

Cria com um comando:

php artisan make:resource UserResource
Enter fullscreen mode Exit fullscreen mode

E define o formato no toArray():

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            'nome'       => $this->name,
            'email'      => $this->email,
            'criado_em'  => $this->created_at->format('d/m/Y'),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Repara no que ganhou de brinde: se não tá na lista, não sai. password nunca vai vazar porque você simplesmente não escreveu ele ali. É seguro por padrão, não por você lembrar de esconder.

E de bônus você controla o formato: renomeou name pra nome, formatou a data. A resposta é sua, não um reflexo direto da tabela.

Como usar no controller

Aí no controller você embrulha o Model no Resource:

public function show(User $user)
{
    return new UserResource($user);
}
Enter fullscreen mode Exit fullscreen mode

Pra uma lista, usa o collection():

public function index()
{
    return UserResource::collection(User::all());
}
Enter fullscreen mode Exit fullscreen mode

Mesmo Resource, seja um registro ou mil. O Laravel cuida de aplicar a transformação em cada item da coleção.

Por padrão a resposta vem embrulhada numa chave data — o que é ótimo, porque te dá espaço pra meta dados (paginação, links) sem quebrar o contrato lá na frente.

O pulo do gato: campos condicionais

Aqui é onde Resource passa longe do $hidden. Às vezes um campo deve aparecer só pra certas pessoas. Tipo um dado que o admin vê e o usuário comum não.

public function toArray(Request $request): array
{
    return [
        'id'    => $this->id,
        'nome'  => $this->name,
        'email' => $this->email,

        // só entra no JSON se quem pediu for admin
        'notas_internas' => $this->when(
            $request->user()?->isAdmin(),
            $this->internal_notes
        ),
    ];
}
Enter fullscreen mode Exit fullscreen mode

O when() só inclui a chave se a condição for verdadeira. Pro usuário comum, o campo nem existe na resposta — não vem como null, não vem vazio, simplesmente não vem. Impossível vazar.

E pra relacionamentos, tem o whenLoaded(), que ainda te salva de N+1:

'posts' => PostResource::collection($this->whenLoaded('posts')),
Enter fullscreen mode Exit fullscreen mode

O relacionamento só entra no JSON se você já tiver carregado ele com with('posts'). Não carregou? Não aparece — e o Laravel não dispara query extra tentando buscar. Segurança e performance no mesmo método.

A pegadinha que confunde muita gente

Dentro do Resource, você acessa os campos com $this->id, $this->name... e parece mágica, porque $this não é o Model, é o Resource.

O que rola é que o Resource encaminha o acesso pro Model que ele está embrulhando. Então $this->name na prática é $user->name. Funciona lindo.

O tropeço é achar que dá pra chamar qualquer método do Model direto no $this sem ele estar carregado, ou esquecer que numa coleção o $this representa cada item, um de cada vez — não a coleção inteira. Se você tentar tratar a coleção como um objeto só ali dentro, vai se enrolar.

Bônus: e agora?

Se você tem Service Layer, o controller fica magrinho: o service resolve a regra, o Resource formata a saída. Cada um no seu quadrado.

Vale também dar uma olhada em make:resource --collection (pra quando a coleção precisa de metadados próprios) e no método additional(), pra pendurar dados extras na resposta sem sujar o toArray().

Antes de você fechar a aba

Faz um teste rápido agora: pega o endpoint mais antigo do seu projeto, aquele que retorna o Model direto, e olha o JSON completo que ele devolve. Tem algum campo ali que não devia estar aparecendo?

Se tem, você acabou de achar seu próximo refactor. E se não tem, provavelmente é porque você teve sorte — não porque teve controle. 🙂

Você usa Resource em tudo ou só quando a resposta é complexa? Conta aí nos comentários como você organiza isso.

E se esse post te fez lembrar de um endpoint suspeito, compartilha com aquele colega que ainda dá return $model sem pensar duas vezes.

Top comments (0)