Unifying API Responses in Laravel — Clean Architecture for Modern Apps

Problem

Most Laravel APIs quickly become inconsistent.
Each controller returns a different JSON structure, different error formats, and different HTTP codes.
Frontend teams struggle because:

  • Responses change from endpoint to endpoint
  • Sometimes errors return errors, sometimes error, sometimes nothing
  • Messages aren’t standardized
  • Backend developers duplicate the same boilerplate everywhere
  • Debugging becomes unnecessarily difficult

As projects grow, this inconsistency creates maintenance nightmares, especially in multi-feature or multi-tenant applications.

Solution

Implement a Unified API Response Trait and extend your Exception Handler to return standardized JSON responses for all exceptions.

Benefits:

  • One single source of truth for all API responses
  • Controllers become cleaner and more maintainable
  • Frontend always receives predictable data
  • Automatic handling of all HTTP status codes
  • Safe ID decryption and error localization
  • Centralized logging and debugging support

Why Unified Responses Matter

A unified response layer sets the foundation for:

  • Clean architecture
  • Predictable frontend behavior
  • Faster debugging
  • Reusable and maintainable API structure
  • Standardization across the whole team
  • Professional API design

Many large systems (SaaS, multi-tenant, multi-module apps) depend on exactly this structure.

What This Trait Provides

  • Unified success response methods (success, successCreate, successUpdate)
  • Unified error response methods (badRequest, unauthorized, notFound, etc.)
  • Safe ID decryption (decryptId())
  • Localization-ready messages (resources/lang/...)
  • Full coverage of all HTTP status codes
  • Cleaner controllers with minimal boilerplate
  • A consistent JSON format:
{
  "status": true | false,
  "message": "",
  "description": "",
  "data": {},
  "errors": {}
}

Controller Usage Example

class UserController
{
    use ApiResponder;

    public function index()
    {
        return $this->success(User::all());
    }

    public function show($encryptedId)
    {
        $id = $this->decryptId($encryptedId);

        $user = User::find($id);

        if (!$user) {
            return $this->notFound();
        }

        return $this->success($user);
    }
}

Full ApiResponder Trait

<?php

namespace App\Traits;

use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\JsonResponse;

trait ApiResponder
{
    protected function success($data = null, ?string $message = null, int $code = 200, ?string $description = null): JsonResponse
    {
        return response()->json([
            'status' => true,
            'message' => $message ?? __('success.success_fetch'),
            'description' => $description,
            'data' => $data,
        ], $code);
    }

    protected function error(?string $message = null, int $code = 500, $errors = null, ?string $description = null): JsonResponse
    {
        return response()->json([
            'status' => false,
            'message' => $message,
            'description' => $description,
            'errors' => $errors,
        ], $code);
    }

    // 🔒 Decrypt ID
    protected function decryptId(string $id)
    {
        try {
            return Crypt::decryptString($id);
        } catch (DecryptException $e) {
            return $this->badRequest('Invalid ID');
        }
    }

    // ❗ Common Error Responses (All Codes)
    protected function badRequest(string $message = 'bad_request', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 400,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function unauthorized(string $message = 'unauthorized', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 401,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function forbidden(string $message = 'forbidden', $errors = null, ?string $description = 'forbidden'): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 403,
            errors: $errors,
            description: __('exceptions.' . $description . '.description')
        );
    }

    protected function notFound(string $message = 'not_found', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 404,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function databaseEntryNotFound(string $message = 'db_entry_not_found', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 404,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function unprocessableEntity(string $message = 'unprocessable_entity', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 422,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function databaseError(string $message = 'database_error', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 500,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function serverError(string $message = 'server_error', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 500,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function notImplemented(string $message = 'not_implemented', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 501,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function serviceUnavailable(string $message = 'service_unavailable', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 503,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function conflict(string $message = 'conflict', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 409,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    // FK violation
    protected function foreignKeyViolation(?string $message = null, $errors = null, ?string $description = null): JsonResponse
    {
        return $this->error(
            message: $message ?? __('exceptions.foreign_key_violation.message'),
            code: 409,
            errors: $errors,
            description: $description ?? __('exceptions.foreign_key_violation.description')
        );
    }

    protected function tooManyRequests(string $message = 'too_many_requests', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 429,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function notAcceptable(string $message = 'not_acceptable', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 406,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function methodNotAllowed(string $message = 'method_not_allowed', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 405,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function paymentRequired(string $message = 'payment_required', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 402,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function gone(string $message = 'gone', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 410,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function preconditionFailed(string $message = 'precondition_failed', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 412,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function tooEarly(string $message = 'too_early', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 425,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function notExtended(string $message = 'not_extended', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 510,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function expectationFailed(string $message = 'expectation_failed', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 417,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function insufficientStorage(string $message = 'insufficient_storage', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 507,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function loopDetected(string $message = 'loop_detected', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 508,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    protected function networkAuthenticationRequired(string $message = 'network_authentication_required', $errors = null): JsonResponse
    {
        return $this->error(
            message: __('exceptions.' . $message . '.message'),
            code: 511,
            errors: $errors,
            description: __('exceptions.' . $message . '.description')
        );
    }

    // Success Responses
    protected function successDelete(string $message = 'success.success_delete', $data = null): JsonResponse
    {
        return response()->json([
            'status' => true,
            'message' => __($message),
            'data' => $data,
        ], 200);
    }

    protected function successUpdate(string $message = 'success.success_update', $data = null): JsonResponse
    {
        return response()->json([
            'status' => true,
            'message' => __($message),
            'data' => $data,
        ], 200);
    }

    protected function successCreate(string $message = 'success.success_create', $data = null): JsonResponse
    {
        return response()->json([
            'status' => true,
            'message' => __($message),
            'data' => $data,
        ], 201);
    }
}

Global Exception Handling with ApiResponder

To apply the unified response system across all API requests, you can integrate the trait into Laravel’s Exception Handler.

This ensures every exception is returned in the same standardized JSON format.

<?php

namespace App\Exceptions;

use Throwable;
use App\Traits\ApiResponder;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\QueryException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Routing\Exceptions\InvalidSignatureException;

class Handler extends ExceptionHandler
{
    use ApiResponder;

    protected $dontReport = [];
    protected $dontFlash = ['password', 'password_confirmation'];

    public function report(Throwable $e)
    {
        parent::report($e);
    }

    public function render($request, Throwable $exception)
    {
        $debug = config('app.debug');
        $getErrorDetails = fn($e) => $debug ? [
            'exception' => get_class($e),
            'message' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
            'trace' => collect($e->getTrace())->take(5)->toArray(),
        ] : null;

        // Handle authentication
        if ($exception instanceof AuthenticationException) {
            return $this->unauthorized(errors: $getErrorDetails($exception));
        }

        // Validation errors
        if ($exception instanceof ValidationException) {
            return $this->unprocessableEntity(errors: $exception->errors());
        }

        // Resource not found
        if ($exception instanceof NotFoundHttpException) {
            if ($exception->getPrevious() instanceof ModelNotFoundException) {
                return $this->databaseEntryNotFound(errors: $getErrorDetails($exception));
            }
            return $this->notFound(errors: $getErrorDetails($exception));
        }

        // Database constraint violations
        if ($exception instanceof QueryException) {
            if (str_contains($exception->getMessage(), 'Integrity constraint violation: 1451')) {
                return $this->foreignKeyViolation();
            }
            return $this->databaseError(errors: $getErrorDetails($exception));
        }

        // Fallback for all other HTTP status codes
        $statusCode = method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500;
        return match($statusCode) {
            400 => $this->badRequest(errors: $getErrorDetails($exception)),
            401 => $this->unauthorized(errors: $getErrorDetails($exception)),
            403 => $this->forbidden(errors: $getErrorDetails($exception)),
            404 => $this->notFound(errors: $getErrorDetails($exception)),
            405 => $this->methodNotAllowed(errors: $getErrorDetails($exception)),
            422 => $this->unprocessableEntity(errors: $getErrorDetails($exception)),
            500, default => $this->serverError(errors: $getErrorDetails($exception)),
        };
    }

    public function register(): void {}
}

Language files

resources/lang/success.php

<?php
return [
    'success_delete' => 'Deleted successfully.',
    'success_update' => 'Updated successfully.',
    'success_create' => 'Created successfully.',
    'success_fetch' => 'Data fetched successfully.',
];

resources/lang/exceptions.php

<?php
return [
    'bad_request' => [
        'message' => 'Bad request. Please check your input.',
        'description' => 'The request could not be understood by the server due to malformed syntax.',
    ],
    'unauthorized' => [
        'message' => 'Authentication Required.',
        'description' => 'You need to sign in to access this page. Please log in with your credentials to continue.',
    ],
    'forbidden' => [
        'message' => 'Access Forbidden',
        'description' => 'You don\'t have permission to access this resource. Contact your administrator if you believe this is an error.',
    ],
    'no_registration_forms' => [
        'message' => 'No Registration Forms Found',
        'description' => 'There are no registration forms available for the selected event participations.',
    ],

    'form_closed' => [
        'message' => 'Registration Form Closed',
        'description' => 'This registration form is currently closed and cannot be accessed.',
    ],
    'not_found' => [
        'message' => 'Page Not Found.',
        'description' => 'The page you\'re looking for doesn\'t exist. It might have been moved, deleted, or you entered the wrong URL.',
    ],
    'server_error' => [
        'message' => 'An unexpected error occurred. Please try again later.',
        'description' => 'Something went wrong on our end. Please try again later or contact support if the problem persists.',
    ],
    'unprocessable_entity' => [
        'message' => 'The data provided is invalid.',
        'description' => 'The request was well-formed but contains semantic errors. Please verify your input data.',
    ],
    'database_error' => [
        'message' => 'Database Error',
        'description' => 'An error occurred while accessing the database. Please try again later or contact support.',
    ],
    'db_entry_not_found' => [
        'message' => 'We couldn’t find what you were looking for.',
        'description' => 'The requested database record was not found.',
    ],
    'conflict' => [
        'message' => 'Conflict occurred.',
        'description' => 'The request could not be completed due to a conflict with the current state of the target resource.',
    ],
    'too_many_requests' => [
        'message' => 'Too many requests.',
        'description' => 'You have made too many requests in a short period. Please slow down and try again later.',
    ],
    'not_acceptable' => [
        'message' => 'Not acceptable.',
        'description' => 'The requested resource is capable of generating only content not acceptable according to the Accept headers sent.',
    ],
    'method_not_allowed' => [
        'message' => 'Method not allowed.',
        'description' => 'The HTTP method used is not allowed for the requested resource.',
    ],
    'payment_required' => [
        'message' => 'Payment required.',
        'description' => 'Access to the requested resource requires payment.',
    ],
    'gone' => [
        'message' => 'Resource is gone.',
        'description' => 'The requested resource is no longer available and will not be available again.',
    ],
    'precondition_failed' => [
        'message' => 'Precondition failed.',
        'description' => 'One or more conditions given in the request header fields evaluated to false when tested on the server.',
    ],
    'too_early' => [
        'message' => 'Request is too early.',
        'description' => 'The server is unwilling to risk processing a request that might be replayed.',
    ],
    'not_extended' => [
        'message' => 'Further extensions to the request are required.',
        'description' => 'Further extensions to the request are required for the server to fulfill it.',
    ],
    'expectation_failed' => [
        'message' => 'Expectation failed.',
        'description' => 'The server cannot meet the requirements of the Expect request-header field.',
    ],
    'insufficient_storage' => [
        'message' => 'Insufficient storage.',
        'description' => 'The server is unable to store the representation needed to complete the request.',
    ],
    'loop_detected' => [
        'message' => 'Loop detected.',
        'description' => 'The server detected an infinite loop while processing the request.',
    ],
    'network_authentication_required' => [
        'message' => 'Network authentication required.',
        'description' => 'The client needs to authenticate to gain network access.',
    ],
    'not_implemented' => [
        'message' => 'This feature is not implemented.',
        'description' => 'The server does not support the functionality required to fulfill the request.',
    ],
    'service_unavailable' => [
        'message' => 'Service is currently unavailable. Please try again later.',
        'description' => 'The server is currently unable to handle the request due to temporary overload or maintenance.',
    ],
    'forbidden_edit' => [
        'message' => 'ُEdit Forbidden',
        'description' => 'You don\'t have permission to edit this resource. Contact your administrator if you believe this is an error.',

    ],
    'forbidden_delete_system_role' => [
        'message' => 'Deleting core system roles is forbidden',
        'description' => 'You do not have permission to delete this resource. Contact the administrator if you believe this is a mistake.',
    ],
    'inactive' => [
        'message' => 'Account Temporarily Disabled',
        'description' => 'Access is currently unavailable for this account. Please reach out to support if you need assistance.',
    ],
    'organization_limit' => [
        'message' => 'Organization participation limit reached',
        'description' => 'Maximum :limit participants reached for this participation level',
    ],
    'edit_token_expired' => [
        'message' => 'Edit session expired',
        'description' => 'The edit link has expired. If you believe this is an error, please contact technical support.',
    ],
    'invalid_otp' => [
        'message' => 'Invalid verification code',
        'description' => 'The code you entered is incorrect. Please check the code and try again.',
    ],
    'otp_expired' => [
        'message' => 'Verification code expired',
        'description' => 'The verification code you entered has expired. Please request a new code and try again.',
    ],
    'foreign_key_violation' => [
        'message' => 'Cannot delete record with dependent data',
        'description' => 'This record cannot be deleted because it has related data in the system. Please delete the dependent records first or contact support.',
    ],
    'invalid_server_api' => [
        'message' => 'Invalid Server API secret',
        'description' => 'The provided API secret is invalid or missing. This action requires proper authentication.',
    ],
    'domain_verification_failed' => [
        'message' => 'Domain verification failed',
        'description' => 'The domain DNS records could not be verified. Please ensure CNAME and TXT records are properly configured and propagated.',
    ],
    'domain_already_exists' => [
        'message' => 'Domain already exists',
        'description' => 'This domain is already registered to another tenant. Each domain can only be assigned to one tenant.',
    ],
];

Final Thoughts

By combining ApiResponder with the custom Exception Handler, you can:

  • Guarantee consistent JSON responses across the API
  • Reduce boilerplate in controllers
  • Standardize error messages and logging
  • Simplify frontend integration
  • Make your API scalable, maintainable, and professional

This setup is ideal for large multi-module systems, SaaS platforms, and multi-tenant applications.

Scroll to Top