DEV Community

Vincent Tommi
Vincent Tommi

Posted on

Refactoring a Laravel API Controller to Reuse AppBaseController Response Helpers

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...
}
Enter fullscreen mode Exit fullscreen mode

Some methods returned manually constructed JSON responses:

return response()->json([
    'success' => true,
    'message' => 'Menu Category created successfully.',
    'data' => new MenuCategoryResource($category),
], 201);
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Each method serves a different purpose:

  • sendResponse() returns a successful response using the project's ResponseUtil.
  • 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;
Enter fullscreen mode Exit fullscreen mode

with:

use App\Http\Controllers\AppBaseController;
Enter fullscreen mode Exit fullscreen mode

Then update the class declaration:

class MenuCategoryController extends AppBaseController
{
    // CRUD methods...
}
Enter fullscreen mode Exit fullscreen mode

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.'
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's examine the important decisions.

A. Paginated listing

$categories = MenuCategory::query()
    ->orderBy('sort_order')
    ->orderBy('name')
    ->paginate(20);

return MenuCategoryResource::collection($categories);
Enter fullscreen mode Exit fullscreen mode

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()
);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

This communicates that a new resource was successfully created.

C. Retrieving a single category

public function show(
    MenuCategory $menuCategory
): MenuCategoryResource {
    return new MenuCategoryResource($menuCategory);
}
Enter fullscreen mode Exit fullscreen mode

Laravel's implicit route model binding resolves the category from the route parameter.

For example:

GET /api/menu-categories/5
Enter fullscreen mode Exit fullscreen mode

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()
);
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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.'
);
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 200 with 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:

  1. Reuse existing architecture. Before introducing a custom response format, inspect the conventions already used by other controllers.
  2. Separate responsibilities. Form requests validate input, models handle persistence and relationships, resources format output, and controllers coordinate the workflow.
  3. Use meaningful HTTP status codes. 201 indicates creation, 404 indicates a missing resource, and 409 can indicate a conflict with the resource's current state.
  4. Avoid unnecessary database work. Relationship exists() checks can be more efficient than loading an entire collection just to count or inspect it.
  5. 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)