Understanding Resource Controllers in Laravel
Resource controllers provide a convenient way to handle CRUD (Create, Read, Update, Delete) operations in your API. Laravel's resource controllers follow RESTful conventions and reduce boilerplate code significantly.
What is a Resource Controller?
A resource controller is a controller that handles all typical CRUD operations for a specific resource. Instead of defining individual routes and controller methods, Laravel provides a standardized approach.
Note: Resource controllers follow RESTful naming conventions, making your API consistent and predictable for consumers.
Creating a Resource Controller
Generate a resource controller using Artisan:
php artisan make:controller Api/PostController --api --resource
The --api flag creates a controller without the create() and edit() methods, which are typically used for rendering forms in web applications but unnecessary for APIs.
Resource Controller Methods
A resource controller includes seven standard methods:
| Method |
HTTP Verb |
URI |
Action |
index() |
GET |
/posts |
Display all posts |
store() |
POST |
/posts |
Create new post |
show() |
GET |
/posts/{post} |
Display specific post |
update() |
PUT/PATCH |
/posts/{post} |
Update specific post |
destroy() |
DELETE |
/posts/{post} |
Delete specific post |
Registering Resource Routes
Register all resource routes with a single line in routes/api.php:
use App\Http\Controllers\Api\PostController;
Route::apiResource('posts', PostController::class);
This single line creates all five routes automatically. You can verify them using:
php artisan route:list --name=posts
Implementing the Index Method
The index() method retrieves and returns a collection of resources:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* Display a listing of posts
*/
public function index()
{
$posts = Post::with('author')
->latest()
->paginate(15);
return response()->json([
'success' => true,
'data' => $posts->items(),
'pagination' => [
'total' => $posts->total(),
'per_page' => $posts->perPage(),
'current_page' => $posts->currentPage(),
'last_page' => $posts->lastPage(),
]
]);
}
}
Tip: Always paginate large collections in your API to improve performance and reduce response sizes.
Implementing the Store Method
The store() method creates a new resource:
/**
* Store a newly created post
*/
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
'tags' => 'nullable|array',
'tags.*' => 'string|max:50',
]);
$post = Post::create([
'title' => $validated['title'],
'content' => $validated['content'],
'category_id' => $validated['category_id'],
'user_id' => auth()->id(),
'published_at' => now(),
]);
if (isset($validated['tags'])) {
$post->tags()->attach($validated['tags']);
}
return response()->json([
'success' => true,
'message' => 'Post created successfully',
'data' => $post->load('author', 'category', 'tags')
], 201);
}
Note: Return HTTP status code 201 (Created) when successfully creating a new resource.
Implementing the Show Method
The show() method retrieves a single resource:
/**
* Display the specified post
*/
public function show(Post $post)
{
// Load relationships
$post->load('author', 'category', 'tags', 'comments.user');
return response()->json([
'success' => true,
'data' => $post
]);
}
Notice how we use route model binding by type-hinting the Post model. Laravel automatically queries the database and returns a 404 if the post doesn't exist.
Tip: Use route model binding to automatically handle model retrieval and 404 responses.
Implementing the Update Method
The update() method modifies an existing resource:
/**
* Update the specified post
*/
public function update(Request $request, Post $post)
{
// Check authorization
if ($post->user_id !== auth()->id()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized to update this post'
], 403);
}
$validated = $request->validate([
'title' => 'sometimes|required|string|max:255',
'content' => 'sometimes|required|string',
'category_id' => 'sometimes|required|exists:categories,id',
'tags' => 'nullable|array',
'tags.*' => 'string|max:50',
'published' => 'sometimes|boolean',
]);
$post->update($validated);
if (isset($validated['tags'])) {
$post->tags()->sync($validated['tags']);
}
return response()->json([
'success' => true,
'message' => 'Post updated successfully',
'data' => $post->load('author', 'category', 'tags')
]);
}
Note: Use sometimes in validation rules for PATCH requests to allow partial updates.
Implementing the Destroy Method
The destroy() method deletes a resource:
/**
* Remove the specified post
*/
public function destroy(Post $post)
{
// Check authorization
if ($post->user_id !== auth()->id()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized to delete this post'
], 403);
}
// Soft delete or force delete
$post->delete();
return response()->json([
'success' => true,
'message' => 'Post deleted successfully'
], 200);
}
Warning: Always implement proper authorization checks before allowing delete operations.
Customizing Resource Routes
You can limit which methods are available:
// Only index and show methods
Route::apiResource('posts', PostController::class)
->only(['index', 'show']);
// All methods except destroy
Route::apiResource('posts', PostController::class)
->except(['destroy']);
Nested Resource Controllers
Handle nested resources (e.g., post comments):
// routes/api.php
Route::apiResource('posts.comments', CommentController::class);
// app/Http/Controllers/Api/CommentController.php
public function index(Post $post)
{
$comments = $post->comments()
->with('user')
->latest()
->paginate(20);
return response()->json([
'success' => true,
'data' => $comments->items(),
'pagination' => [
'total' => $comments->total(),
'current_page' => $comments->currentPage(),
]
]);
}
public function store(Request $request, Post $post)
{
$validated = $request->validate([
'content' => 'required|string|max:1000',
]);
$comment = $post->comments()->create([
'content' => $validated['content'],
'user_id' => auth()->id(),
]);
return response()->json([
'success' => true,
'message' => 'Comment added successfully',
'data' => $comment->load('user')
], 201);
}
Shallow Nesting
For deeply nested resources, use shallow nesting to keep URLs manageable:
Route::apiResource('posts.comments', CommentController::class)
->shallow();
// This generates:
// GET /posts/{post}/comments - index
// POST /posts/{post}/comments - store
// GET /comments/{comment} - show
// PUT /comments/{comment} - update
// DELETE /comments/{comment} - destroy
Advanced Filtering and Sorting
Enhance your index method with filtering and sorting:
public function index(Request $request)
{
$query = Post::with('author', 'category');
// Filter by category
if ($request->has('category')) {
$query->where('category_id', $request->category);
}
// Filter by author
if ($request->has('author')) {
$query->where('user_id', $request->author);
}
// Search by title or content
if ($request->has('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%");
});
}
// Sorting
$sortBy = $request->get('sort_by', 'created_at');
$sortOrder = $request->get('sort_order', 'desc');
$query->orderBy($sortBy, $sortOrder);
$posts = $query->paginate(
$request->get('per_page', 15)
);
return response()->json([
'success' => true,
'data' => $posts->items(),
'pagination' => [
'total' => $posts->total(),
'per_page' => $posts->perPage(),
'current_page' => $posts->currentPage(),
'last_page' => $posts->lastPage(),
]
]);
}
Exercise:
- Create a resource controller for a "Product" model with all CRUD operations
- Implement filtering by price range and category in the index method
- Add authorization checks to ensure only product owners can update/delete their products
- Create a nested resource for product reviews (
/products/{product}/reviews)
- Test all endpoints using Postman or curl
Best Practices
- Use Route Model Binding: Automatically inject model instances instead of manually querying
- Return Appropriate Status Codes: 200 for success, 201 for creation, 204 for deletion, 404 for not found
- Implement Authorization: Always verify user permissions before allowing operations
- Eager Load Relationships: Use
with() to prevent N+1 query problems
- Paginate Collections: Never return unbounded collections in production
- Validate Input: Always validate incoming data before processing
- Use Transactions: Wrap complex operations in database transactions
Summary
Resource controllers provide a standardized, RESTful approach to building APIs in Laravel. By following conventions and best practices, you create predictable, maintainable APIs that are easy for consumers to understand and use. In the next lesson, we'll explore API Resources for transforming model data into JSON responses.