Refactoring a Laravel API Controller to Reuse AppBaseController Response Helpers
When building APIs in Laravel, it's easy to create a controller that works correctly but doesn't follow the response conventions already established in a project.
I recently worked on a MenuCategoryController that handled CRUD operations, pagination, validation, and deletion checks. However, it returned custom JSON responses instead of following the existing AppBaseController pattern used by other controllers in the application.
In this article, I'll explain how to refactor a Laravel API controller to reuse shared response helpers while preserving resource-based responses and proper HTTP status codes.
The Problem: Inconsistent API Responses
My original controller extended Laravel's default Controller:
use App\Http\Controllers\Controller;
class MenuCategoryController extends Controller
{
// CRUD methods...
}
Some methods returned manually constructed JSON responses:
return response()->json([
'success' => true,
'message' => 'Menu Category created successfully.',
'data' => new MenuCategoryResource($category),
], 201);
Although this works, manually constructing response bodies in multiple controllers can introduce inconsistencies.
For example, one controller might return a success field, while another uses a shared response utility. Over time, this makes the API harder to maintain and its responses less predictable for frontend developers.
The project already had an AppBaseController with reusable response methods, so the better approach was to reuse the existing architecture.
Step 1: Understand the Existing AppBaseController
The existing base controller provides three response helpers:
class AppBaseController extends Controller
{
public function sendResponse($result, $message): JsonResponse
{
return Response::json(
ResponseUtil::makeResponse($message, $result)
);
}
public function sendError($error, $code = 422): JsonResponse
{
return Response::json(
ResponseUtil::makeError($error),
$code
);
}
public function sendSuccess($message): JsonResponse
{
return Response::json([
'success' => true,
'message' => $message,
], 200);
}
}
Each method serves a different purpose:
-
sendResponse()returns a successful response using the project'sResponseUtil. -
sendError()returns an error response with a configurable HTTP status code. -
sendSuccess()returns a simple success message.
These helpers establish shared response conventions and reduce duplicated response-handling code.
Important: sendResponse() and sendSuccess() do not return identical JSON structures. Always inspect ResponseUtil::makeResponse() and ResponseUtil::makeError() before assuming their exact output format.
Step 2: Extend AppBaseController
The first change is to replace the default controller import:
use App\Http\Controllers\Controller;
with:
use App\Http\Controllers\AppBaseController;
Then update the class declaration:
class MenuCategoryController extends AppBaseController
{
// CRUD methods...
}
The controller can now access the shared response helpers without redefining them.
This is a simple example of code reuse and inheritance in PHP: common behavior belongs in a shared parent class, while the child controller focuses on its resource-specific business logic.
Step 3: Implement the CRUD Operations
Below is the refactored controller.
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\AppBaseController;
use App\Http\Requests\StoreMenuCategoryRequest;
use App\Http\Requests\UpdateMenuCategoryRequest;
use App\Http\Resources\MenuCategoryResource;
use App\Models\MenuCategory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class MenuCategoryController extends AppBaseController
{
/**
* Display a paginated listing of menu categories.
*/
public function index(): AnonymousResourceCollection
{
$categories = MenuCategory::query()
->orderBy('sort_order')
->orderBy('name')
->paginate(20);
return MenuCategoryResource::collection($categories);
}
/**
* Store a newly created menu category.
*/
public function store(
StoreMenuCategoryRequest $request
): JsonResponse {
$category = MenuCategory::create(
$request->validated()
);
return (new MenuCategoryResource($category))
->response()
->setStatusCode(201);
}
/**
* Display the specified menu category.
*/
public function show(
MenuCategory $menuCategory
): MenuCategoryResource {
return new MenuCategoryResource($menuCategory);
}
/**
* Update the specified menu category.
*/
public function update(
UpdateMenuCategoryRequest $request,
MenuCategory $menuCategory
): MenuCategoryResource {
$menuCategory->update(
$request->validated()
);
return new MenuCategoryResource(
$menuCategory->refresh()
);
}
/**
* Remove the specified menu category.
*/
public function destroy(
MenuCategory $menuCategory
): JsonResponse {
if ($menuCategory->menuItems()->exists()) {
return $this->sendError(
'Menu category cannot be deleted because it has menu items.',
409
);
}
$menuCategory->delete();
return $this->sendSuccess(
'Menu category deleted successfully.'
);
}
}
Let's examine the important decisions.
A. Paginated listing
$categories = MenuCategory::query()
->orderBy('sort_order')
->orderBy('name')
->paginate(20);
return MenuCategoryResource::collection($categories);
Pagination prevents the API from returning every category in a single request. This becomes increasingly important as the database grows.
Ordering by sort_order and then name also provides predictable category ordering.
Laravel's resource collection preserves pagination metadata, although the precise JSON envelope depends on the resource configuration and Laravel version.
B. Validated input when creating a category
$category = MenuCategory::create(
$request->validated()
);
The StoreMenuCategoryRequest handles validation before the controller method executes.
Using validated() means the controller receives only fields accepted by the request's validation rules, rather than blindly accepting every submitted field.
The model must still have the appropriate mass-assignment configuration, and authorization rules should be applied where required.
The response uses HTTP 201 Created:
return (new MenuCategoryResource($category))
->response()
->setStatusCode(201);
This communicates that a new resource was successfully created.
C. Retrieving a single category
public function show(
MenuCategory $menuCategory
): MenuCategoryResource {
return new MenuCategoryResource($menuCategory);
}
Laravel's implicit route model binding resolves the category from the route parameter.
For example:
GET /api/menu-categories/5
Laravel attempts to resolve the category with ID 5. If it cannot find the model, the framework normally returns a 404 Not Found response.
Returning a resource also keeps the category's JSON representation under the control of MenuCategoryResource.
D. Updating a category
$menuCategory->update(
$request->validated()
);
return new MenuCategoryResource(
$menuCategory->refresh()
);
The update request validates the incoming fields before the model is changed.
Calling refresh() reloads the model's current database values before constructing the response. This can be useful when the database applies defaults or other changes during persistence.
E. Preventing deletion of categories with menu items
if ($menuCategory->menuItems()->exists()) {
return $this->sendError(
'Menu category cannot be deleted because it has menu items.',
409
);
}
Deleting a category that still has associated menu items could break application behavior or violate database constraints.
The exists() query checks whether any related records exist without loading the entire relationship collection into memory.
HTTP 409 Conflict is appropriate when the requested deletion conflicts with the current state of the resource.
After the deletion check passes:
$menuCategory->delete();
return $this->sendSuccess(
'Menu category deleted successfully.'
);
The controller reuses the shared success helper instead of constructing another custom JSON response.
Step 4: Understand Where Resources and Shared Helpers Fit
A useful distinction is that resource formatting and shared response helpers solve related but different problems.
| Component | Responsibility |
|---|---|
AppBaseController |
Shared response helper methods |
MenuCategoryController |
Handles CRUD workflow and business rules |
StoreMenuCategoryRequest |
Validates creation input |
UpdateMenuCategoryRequest |
Validates update input |
MenuCategoryResource |
Controls how a category is represented in JSON |
MenuCategory model |
Represents the database entity and relationships |
In this implementation, the listing, creation, retrieval, and update methods return Laravel resources directly, while deletion uses the shared sendSuccess() and sendError() helpers.
That matches the resource-oriented style used by the existing RoleAPIController.
However, if the project requires every successful endpoint to use ResponseUtil::makeResponse(), then the resource-returning methods should also be adapted to that convention. Before doing so, check whether the existing API expects Laravel's standard resource envelope or the custom envelope defined by ResponseUtil.
Consistency means following the actual project contract, not simply using the same method everywhere.
Step 5: Register the CRUD Routes
Laravel can register the standard API resource routes with one declaration:
use App\Http\Controllers\API\MenuCategoryController;
use Illuminate\Support\Facades\Route;
Route::apiResource(
'menu-categories',
MenuCategoryController::class
);
This generates the five standard API operations:
| HTTP method | Endpoint | Controller method |
|---|---|---|
| GET | /api/menu-categories |
index |
| POST | /api/menu-categories |
store |
| GET | /api/menu-categories/{menu_category} |
show |
| PUT/PATCH | /api/menu-categories/{menu_category} |
update |
| DELETE | /api/menu-categories/{menu_category} |
destroy |
The actual route parameter name can depend on Laravel's resource parameter configuration. Confirm the registered routes using Artisan.
Step 6: Verify the Changes
After saving the controller, run:
php artisan optimize:clear
php artisan route:list --path=menu-categories
Then test the endpoints with Postman or your preferred API client.
Verify that:
- The listing is paginated.
- Invalid creation and update requests are rejected.
- Successful creation returns HTTP
201. - Requests for nonexistent categories return
404. - Categories with related menu items cannot be deleted.
- Successful deletion returns HTTP
200with the shared success message. - Response structures match the conventions expected by the frontend.
You should also test duplicate category names. The controller shown here does not itself prevent duplicate creation; that behavior must be implemented through validation and appropriate database constraints.
Lessons Learned
This refactoring reinforced several backend development principles:
- Reuse existing architecture. Before introducing a custom response format, inspect the conventions already used by other controllers.
- Separate responsibilities. Form requests validate input, models handle persistence and relationships, resources format output, and controllers coordinate the workflow.
-
Use meaningful HTTP status codes.
201indicates creation,404indicates a missing resource, and409can indicate a conflict with the resource's current state. -
Avoid unnecessary database work. Relationship
exists()checks can be more efficient than loading an entire collection just to count or inspect it. - Treat API responses as a contract. Frontend applications depend on predictable response shapes, pagination metadata, and error handling.
Conclusion
Refactoring MenuCategoryController to extend AppBaseController improves alignment with the existing Laravel application and makes shared response behavior easier to maintain.
The key is to reuse the project's established conventions without sacrificing validation, pagination, resource formatting, or correct HTTP semantics.
Before applying the same pattern to other controllers, inspect their response contracts and confirm whether they use Laravel resources, ResponseUtil, or a combination of both.
That small step can prevent a lot of inconsistent API behavior as an application grows.
Top comments (0)