JSON Resources provide a dedicated transformation layer between your Eloquent models and the JSON responses your API returns. This lets you control exactly what fields are exposed, add computed properties, include related data, and maintain a consistent API contract without putting transformation logic inside your controllers.
Why Use Resources?
Without resources, your controller might directly return model data:
example.php
PHP
// ❌ Bad: exposes all fields including sensitive ones (password, email, etc.)
return Response::json($user->toArray());With resources, you control the output explicitly:
example.php
PHP
// ✅ Good: clean, controlled API response
return (new UserResource($user))->toResponse();Creating a Resource
Terminal • bash
php veldora make:resource PostResource
php veldora make:resource UserResourceDefining a Resource
app/Models/Post.php
PHP
// app/Http/Resources/PostResource.php
namespace App\Http\Resources;
use Veldora\Framework\Http\Resources\JsonResource;
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => substr($this->body, 0, 120) . '...',
'published_at' => $this->published_at?->format('Y-m-d H:i:s'),
'is_published' => (bool) $this->is_published,
'author' => [
'id' => $this->author->id,
'name' => $this->author->name,
'avatar' => storage('public')->url($this->author->avatar_path ?? 'default.png'),
],
'meta' => [
'comment_count' => $this->comments()->count(),
],
];
}
}Returning Single Resources
routes/web.php
PHP
// routes/web.php
$router->get('/api/posts/{id}', [PostController::class, 'show']);
// app/Controllers/PostController.php
public function show(string $id): Response
{
$post = Post::find((int) $id);
if (! $post) {
return Response::json(['error' => 'Post not found'], 404);
}
return (new PostResource($post))->toResponse();
}Response:
package.json
JSON
{
"id": 1,
"title": "Hello Veldora",
"slug": "hello-veldora",
"excerpt": "Veldora is a modern PHP framework...",
"published_at": "2026-08-23 14:00:00",
"is_published": true,
"author": { "id": 5, "name": "Jane Doe", "avatar": "/storage/avatars/user-5.png" },
"meta": { "comment_count": 12 }
}Returning Resource Collections
example.php
PHP
// Return all posts as a collection
public function index(Request $request): Response
{
$posts = Post::where('is_published', '=', 1)
->orderBy('created_at', 'DESC')
->get();
return PostResource::collection($posts)->toResponse();
}Returning Paginated Collections
Pass a paginator instead of a collection to automatically include meta and links:
example.php
PHP
public function index(Request $request): Response
{
$page = (int) $request->query('page', 1);
$paginator = Post::where('is_published', '=', 1)->paginate(15, $page);
return PostResource::collection($paginator)->toResponse();
}Response:
package.json
JSON
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 15,
"total": 120,
"last_page": 8
},
"links": {
"first": "/api/posts?page=1",
"last": "/api/posts?page=8",
"prev": null,
"next": "/api/posts?page=2"
}
}Building a Complete REST API
routes/web.php
PHP
// routes/web.php — RESTful API routes
$router->group(['prefix' => '/api'], function ($r) {
$r->get('/posts', [PostController::class, 'index']); // GET /api/posts
$r->post('/posts', [PostController::class, 'store']); // POST /api/posts
$r->get('/posts/{id}', [PostController::class, 'show']); // GET /api/posts/1
$r->put('/posts/{id}', [PostController::class, 'update']); // PUT /api/posts/1
$r->delete('/posts/{id}',[PostController::class, 'destroy']); // DELETE /api/posts/1
});
// app/Controllers/PostController.php
class PostController
{
public function index(Request $request): Response
{
$page = (int) $request->query('page', 1);
$posts = Post::where('is_published', '=', 1)->paginate(15, $page);
return PostResource::collection($posts)->toResponse();
}
public function store(Request $request): Response
{
$data = $request->validated([
'title' => 'required|min:3|max:255',
'body' => 'required|min:10',
]);
$post = Post::create([
...$data,
'user_id' => auth()->id(),
'slug' => strtolower(str_replace(' ', '-', $data['title'])),
'is_published' => 1,
]);
return (new PostResource($post))->toResponse();
}
public function update(string $id, Request $request): Response
{
$post = Post::find((int) $id);
if (! $post || $post->user_id !== auth()->id()) {
return Response::json(['error' => 'Not found or unauthorized'], 403);
}
$data = $request->validated([
'title' => 'required|min:3|max:255',
'body' => 'required|min:10',
]);
$post->title = $data['title'];
$post->body = $data['body'];
$post->save();
return (new PostResource($post))->toResponse();
}
public function destroy(string $id): Response
{
$post = Post::find((int) $id);
if (! $post || $post->user_id !== auth()->id()) {
return Response::json(['error' => 'Not found or unauthorized'], 403);
}
$post->delete();
return Response::json(['message' => 'Post deleted successfully']);
}
}