REST API Development

Resource Controllers & CRUD Operations

20 min Lesson 6 of 35

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:
  1. Create a resource controller for a "Product" model with all CRUD operations
  2. Implement filtering by price range and category in the index method
  3. Add authorization checks to ensure only product owners can update/delete their products
  4. Create a nested resource for product reviews (/products/{product}/reviews)
  5. 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.