The first time I shipped a client dashboard against my own API, a user row in the frontend console contained password: "$2y$12$..." and remember_token: "...". Nothing leaked publicly — it was my own fetch — but it made the problem obvious: I was returning the Eloquent model straight from the controller, and the model's entire attribute bag went over the wire. Whatever $hidden said that day.
In my starter kit there is exactly one place that decides what a user looks like over the wire, and it's not the model:
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->role,
'email_verified_at' => $this->email_verified_at,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Seven fields. No password hash, no remember token, nothing a client never asked for. And the important part isn't the list — it's that every user-returning endpoint goes through it. Register and login return {user: new UserResource($user), token: $token}. The /auth/me endpoint returns {user: new UserResource($request->user())}. The admin users controller returns UserResource::collection($users) for the paginated index and new UserResource($user) on store, show, and update. One shape, everywhere.
That uniformity is what my React code actually depends on. The auth context does setUser(res.data.user) after login, register, and the boot-time /me restore without a second thought — it never has to ask "which endpoint did this come from?" The demo mock mirrors the same shape so the offline demo behaves identically. If I later add a field to the user payload, there's one file to change and zero frontend conditionals to audit.
Could $hidden on the model do this? Mostly, yes, until the day you need one representation for the admin list and a different one for the public profile — then you're mutating a global to fix a local problem. The resource keeps presentation in the presentation layer. It costs one small file per model and buys you a contract you can actually reason about.
I put this pattern in the starter kit I sell (the Laravel + React SaaS Starter Kit, $19 one-time: https://kamranofficial.gumroad.com/l/mhekoig) because it's the kind of boring decision that prevents exciting incidents. Your API's shape is a promise; write it down in a resource instead of inheriting it from your database schema.
Top comments (0)