Skip to content

Laresponse (bpmlib/laravel-laresponse)

Laravel trait untuk standardized JSON responses dengan support pagination dan validation errors

Versi: 0.1.2

PHPLaravel


TL;DR

Laravel Laresponse menyediakan trait JsonResponseTrait yang standardize format JSON responses di Laravel. Trait ini menangani berbagai tipe responses: single data, lists, pagination (LengthAware, Cursor, Simple), validation errors, dan data transformation. Semua response mengikuti struktur consistent dengan message, content, dan success fields.

Namespace:

php
use Bpmlib\Laresponse\Traits\JsonResponseTrait;

Response Methods (Quick Reference):

  • returnAdaptive() - ⭐ RECOMMENDED untuk list/collection responses (auto-detect pagination type)
  • returnJson() - ⭐ RECOMMENDED untuk single data atau error responses
  • returnPaginateJson() - Explicit untuk LengthAware pagination
  • returnCursorPaginateJson() - Explicit untuk Cursor pagination
  • returnSimplePaginateJson() - Explicit untuk Simple pagination

Response Format:

json
{
  "message": "Sukses",
  "content": { ... },
  "success": true,
  "validation": { ... }
}

Installation & Setup

Requirements

PHP Version

  • Minimum: PHP 8.1
  • Recommended: PHP 8.3+

Composer Dependencies

Package ini memerlukan Laravel components:

json
{
  "illuminate/support": "^10.0|^11.0|^12.0",
  "illuminate/http": "^10.0|^11.0|^12.0",
  "illuminate/pagination": "^10.0|^11.0|^12.0",
  "illuminate/database": "^10.0|^11.0|^12.0"
}

Framework Requirements

  • Laravel: 10.0+, 11.0+, atau 12.0+

Composer Install

bash
composer require bpmlib/laravel-laresponse

Auto-Discovery

Tidak ada service provider atau config file. Trait langsung ready to use setelah installation.


Quick Start

Basic Usage - Single Data

Contoh paling sederhana untuk single resource response.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\User;

class UserController extends Controller
{
    use JsonResponseTrait;

    public function show($id)
    {
        $user = User::find($id);
        
        if (!$user) {
            return $this->returnJson([], 404, 'User not found');
        }
        
        return $this->returnJson($user);
    }
}

Output (Success - 200):

json
{
  "message": "Sukses",
  "content": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "success": true
}

Output (Error - 404):

json
{
  "message": "User not found",
  "content": [],
  "success": false
}

Key Points:

  • returnJson() untuk single data atau error responses
  • HTTP code menentukan success field automatically
  • Custom message atau gunakan default message per status code

Gunakan returnAdaptive() untuk semua list/collection responses. Method ini auto-detect tipe pagination.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;

class ProductController extends Controller
{
    use JsonResponseTrait;

    public function index()
    {
        // Option 1: Auto-detect Array/Collection
        $products = Product::all();
        return $this->returnAdaptive($products);
        
        // Option 2: Auto-detect LengthAwarePaginator
        $products = Product::paginate(15);
        return $this->returnAdaptive($products);
        
        // Option 3: Auto-detect CursorPaginator
        $products = Product::cursorPaginate(15);
        return $this->returnAdaptive($products);
        
        // Option 4: Auto-detect Paginator (Simple)
        $products = Product::simplePaginate(15);
        return $this->returnAdaptive($products);
    }
}

Output (LengthAware Pagination):

json
{
  "message": "Sukses",
  "content": [
    { "id": 1, "name": "Product 1" },
    { "id": 2, "name": "Product 2" }
  ],
  "success": true,
  "max_page": 10,
  "current_page": 1,
  "per_page": 15,
  "total": 150,
  "has_more": true,
  "next_cursor": null,
  "previous_cursor": null
}

Key Points:

  • Satu method untuk semua tipe list (array, Collection, atau pagination)
  • Auto-detect pagination type dan return appropriate metadata
  • Consistent response structure untuk berbagai tipe data

With Validation Errors

Handle validation errors dengan returnJson() untuk status 422.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Models\User;

class UserController extends Controller
{
    use JsonResponseTrait;

    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|min:3',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]);
        
        if ($validator->fails()) {
            return $this->returnJson(
                content: [],
                code: 422,
                message: 'Validasi gagal',
                validationErrors: $validator->errors()
            );
        }
        
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password),
        ]);
        
        return $this->returnJson($user, 201, 'User berhasil dibuat');
    }
}

Output (Validation Failed - 422):

json
{
  "message": "Validasi gagal",
  "content": [],
  "success": false,
  "validation": {
    "name": ["The name field is required."],
    "email": ["The email must be a valid email address."]
  }
}

Output (Success - 201):

json
{
  "message": "User berhasil dibuat",
  "content": {
    "id": 5,
    "name": "Jane Smith",
    "email": "jane@example.com"
  },
  "success": true
}

Key Points:

  • validationErrors parameter exclusive untuk returnJson()
  • Validation errors hanya muncul di response jika HTTP code = 422
  • Accepts MessageBag atau array format

With Data Mapper

Transform data sebelum dikirim ke frontend menggunakan $mapper callback.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\User;

class UserController extends Controller
{
    use JsonResponseTrait;

    public function index()
    {
        $users = User::with('profile')->paginate(10);
        
        return $this->returnAdaptive(
            content: $users,
            mapper: function($user) {
                return [
                    'id' => $user->id,
                    'name' => $user->name,
                    'email' => $user->email,
                    'avatar' => $user->profile?->avatar_url ?? '/default-avatar.png',
                    'member_since' => $user->created_at->diffForHumans(),
                    'verified' => (bool) $user->email_verified_at,
                ];
            }
        );
    }
}

Output:

json
{
  "message": "Sukses",
  "content": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com",
      "avatar": "https://example.com/avatars/john.jpg",
      "member_since": "2 months ago",
      "verified": true
    }
  ],
  "success": true,
  "max_page": 5,
  "current_page": 1,
  "per_page": 10,
  "total": 50,
  "has_more": true
}

Key Points:

  • Mapper callback applied ke setiap item di array/Collection/Paginator
  • Works dengan returnJson() dan returnAdaptive()
  • Clean separation: data transformation di response layer, bukan di Model
  • Useful untuk hide sensitive fields atau compute derived properties

Core Concepts

Standardized Response Structure

Semua responses mengikuti struktur consistent untuk memudahkan frontend parsing.

Base Structure:

php
[
    'message' => string,   // Human-readable message
    'content' => mixed,    // Main response data
    'success' => bool,     // Auto-determined dari HTTP code (2xx = true)
]

Extended Structure (Pagination):

php
[
    'message' => string,
    'content' => array,
    'success' => bool,
    
    // LengthAware Pagination
    'max_page' => int,
    'current_page' => int,
    'per_page' => int,
    'total' => int,
    'has_more' => bool,
    
    // Cursor Pagination
    'next_cursor' => ?string,
    'previous_cursor' => ?string,
]

Extended Structure (Validation Errors):

php
[
    'message' => string,
    'content' => [],
    'success' => false,
    'validation' => [
        'field_name' => ['error message 1', 'error message 2']
    ]
]

Implications:

  • Frontend selalu bisa expect message, content, success fields
  • success field eliminate need untuk check HTTP status di frontend
  • Pagination metadata consistent across different pagination types (unused fields set to null)

Method Selection Guide

Library ini menyediakan multiple methods dengan different trade-offs. Pahami kapan menggunakan masing-masing.

returnAdaptive() vs Specific Methods

returnAdaptive() (Recommended Default):

  • ✅ Auto-detect tipe pagination (LengthAware, Cursor, Simple, atau plain array)
  • ✅ Flexible untuk berbagai tipe data
  • ✅ Single method untuk semua list responses
  • ✅ Best untuk rapid development dan dynamic queries
  • ⚠️ Sedikit overhead untuk type checking (negligible untuk most cases)

Specific Methods (returnXxxPaginateJson):

  • ✅ Explicit type - no auto-detection overhead
  • ✅ Type safety jika kamu yakin exact pagination type
  • ✅ Useful untuk strict API contracts (OpenAPI/Swagger)
  • ✅ Code clarity - explicit intent
  • ⚠️ Harus tahu pagination type sebelumnya

When to Use returnAdaptive()

Use cases:

  • ✅ Default choice untuk list responses
  • ✅ Working dengan dynamic queries yang bisa paginate atau tidak
  • ✅ Prototyping atau rapid development
  • ✅ Pagination type bisa berubah based on requirements
  • ✅ Flexible API responses (mobile + web dengan different pagination needs)

Example:

php
// Query bisa return paginated atau all items based on request
public function index(Request $request)
{
    $query = Product::query();
    
    if ($request->has('paginate')) {
        $products = $query->paginate($request->input('per_page', 15));
    } else {
        $products = $query->get();
    }
    
    // returnAdaptive handles both cases
    return $this->returnAdaptive($products);
}

When to Use returnJson()

Use cases:

  • ✅ Single resource responses (User::find(), Product::first())
  • ✅ Error responses (404, 500, 403, dll)
  • ✅ Validation errors (422) - exclusive feature
  • ✅ Success responses dengan single item
  • ✅ Empty success responses (204, 201 after create/delete)

Example:

php
// Single resource
public function show($id)
{
    $user = User::find($id);
    return $this->returnJson($user);
}

// Error response
public function destroy($id)
{
    $product = Product::find($id);
    
    if (!$product) {
        return $this->returnJson([], 404, 'Product not found');
    }
    
    $product->delete();
    return $this->returnJson([], 200, 'Product deleted successfully');
}

// Validation error (exclusive to returnJson)
public function store(Request $request)
{
    $validator = Validator::make($request->all(), [...]);
    
    if ($validator->fails()) {
        return $this->returnJson([], 422, validationErrors: $validator->errors());
    }
    // ...
}

When to Use Specific Pagination Methods

Use cases:

  • ✅ API contract explicitly requires specific pagination type
  • ✅ Performance critical endpoints (high traffic, avoid type checking)
  • ✅ Code clarity preferred over flexibility
  • ✅ Team convention uses explicit typing
  • ✅ Documented API (OpenAPI/Swagger) dengan fixed pagination schema

Example:

php
// API contract guarantees LengthAware pagination
public function index()
{
    $products = Product::paginate(15); // Always LengthAwarePaginator
    return $this->returnPaginateJson($products);
}

// Infinite scroll API always uses Cursor
public function feed()
{
    $posts = Post::latest()->cursorPaginate(20);
    return $this->returnCursorPaginateJson($posts);
}

Decision Flowchart

Start
  |
  +-- Single data/error? -> returnJson()
  |
  +-- Validation error (422)? -> returnJson() (exclusive)
  |
  +-- List/Collection?
  |    |
  |    +-- Flexible/unknown pagination? -> returnAdaptive()
  |    |
  |    +-- Strict API contract? -> returnXxxPaginateJson()
  |    |
  |    +-- Performance critical? -> returnXxxPaginateJson()
  |
  +-- Default -> returnAdaptive()

Automatic Message Resolution

Trait menyediakan default messages untuk common HTTP status codes, eliminate boilerplate message strings.

Default Messages:

php
[
    200 => 'Sukses',
    201 => 'Sukses dibuat',
    400 => 'Pastikan format request yang Anda kirimkan sesuai',
    401 => 'Anda belum login',
    403 => 'Anda tidak mempunyai akses untuk ini',
    404 => 'Data yang anda cari tidak ada',
    422 => 'Form yang anda kirimkan ada yang tidak valid',
    500 => 'Terjadi kesalahan di server',
    503 => 'Server sedang sibuk',
]

How it works:

php
// Empty message - uses default
return $this->returnJson($user, 404);
// Output: { "message": "Data yang anda cari tidak ada", ... }

// Custom message - overrides default
return $this->returnJson($user, 404, 'User tidak ditemukan');
// Output: { "message": "User tidak ditemukan", ... }

// Status code not in dictionary - fallback message
return $this->returnJson($user, 418);
// Output: { "message": "Hai :D", ... }

Benefits:

  • Consistent messages across application
  • Less boilerplate code
  • Easy to customize per-response when needed
  • Fallback message untuk unknown status codes

Data Mapping Pattern

Mapper callback memungkinkan data transformation sebelum dikirim ke response, clean separation antara data layer dan presentation layer.

How it works:

Mapper callback dipanggil untuk setiap item:

  • Array (list): array_map($mapper, $array)
  • Array (associative): $mapper($array)
  • Collection: $collection->map($mapper)
  • Paginator: $paginator->getCollection()->map($mapper)

Common use cases:

  1. Hide sensitive fields:
php
return $this->returnAdaptive($users, mapper: fn($u) => [
    'id' => $u->id,
    'name' => $u->name,
    // Hide: email, password_hash, api_token
]);
  1. Compute derived properties:
php
return $this->returnAdaptive($products, mapper: fn($p) => [
    'id' => $p->id,
    'name' => $p->name,
    'price' => $p->price,
    'discount_price' => $p->price * 0.9,
    'is_available' => $p->stock > 0,
]);
  1. Format dates/timestamps:
php
return $this->returnAdaptive($orders, mapper: fn($o) => [
    'id' => $o->id,
    'total' => $o->total,
    'created' => $o->created_at->format('Y-m-d H:i:s'),
    'created_human' => $o->created_at->diffForHumans(),
]);
  1. Include related data:
php
return $this->returnAdaptive($posts->load('author'), mapper: fn($p) => [
    'id' => $p->id,
    'title' => $p->title,
    'author' => [
        'id' => $p->author->id,
        'name' => $p->author->name,
    ],
]);

Best practices:

  • Use mapper untuk presentation logic, bukan business logic
  • Keep mappers simple dan readable
  • Extract ke dedicated methods jika mapper complex:
php
public function index()
{
    return $this->returnAdaptive(
        Product::paginate(15),
        mapper: [$this, 'mapProductForApi']
    );
}

private function mapProductForApi(Product $product): array
{
    return [
        'id' => $product->id,
        'name' => $product->name,
        // ... complex mapping logic
    ];
}

Append Mechanism

Parameter $appends memungkinkan merge additional data ke response tanpa override built-in keys.

How it works:

php
$appends = [
    'meta' => ['api_version' => '2.0'],
    'message' => 'This will be ignored', // Cannot override
];

return $this->returnJson($data, appends: $appends);

Built-in keys (message, content, success, validation, pagination metadata) cannot be overridden oleh $appends. System menggunakan array_diff_key() untuk filter out conflicts.

Common use cases:

  1. API metadata:
php
return $this->returnAdaptive($products, appends: [
    'meta' => [
        'api_version' => '2.0',
        'timestamp' => now()->toIso8601String(),
        'server_time' => now()->timestamp,
    ],
]);
  1. Applied filters info:
php
return $this->returnAdaptive($products, appends: [
    'filters' => [
        'category' => request('category'),
        'min_price' => request('min_price'),
        'max_price' => request('max_price'),
    ],
]);
  1. Aggregated data:
php
return $this->returnAdaptive($orders, appends: [
    'summary' => [
        'total_amount' => $orders->sum('total'),
        'average_amount' => $orders->avg('total'),
        'count' => $orders->count(),
    ],
]);
  1. Debug information (development only):
php
if (app()->environment('local')) {
    $appends['debug'] = [
        'query_count' => DB::getQueryLog(),
        'memory_usage' => memory_get_peak_usage(true),
    ];
}

return $this->returnAdaptive($data, appends: $appends);

Protection mechanism:

php
// These keys are protected and cannot be overridden:
$protected = [
    'message', 'content', 'success', 'validation',
    'max_page', 'current_page', 'per_page', 'total', 'has_more',
    'next_cursor', 'previous_cursor',
];

API Reference

JsonResponseTrait

Trait untuk standardize JSON responses di Laravel controllers.

Namespace: Bpmlib\Laresponse\Traits\JsonResponseTrait

Contains:


returnAdaptive()

Auto-detect tipe data (array, Collection, LengthAware, Cursor, Simple) dan return appropriate response format.

Use cases:

  • Default choice untuk list/collection responses
  • Dynamic queries yang bisa return different pagination types
  • Flexible APIs yang support multiple response formats

Signature:

php
protected function returnAdaptive(
    Collection|LengthAwarePaginator|CursorPaginator|Paginator|array $content,
    string $message = '',
    int $code = 200,
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentCollection|LengthAwarePaginator|CursorPaginator|Paginator|arrayrequiredData yang akan di-return Lihat selengkapnya
$messagestring''Custom message atau gunakan default per HTTP code
$codeint200HTTP status code
$mapper?callablenullCallback untuk transform data Lihat selengkapnya
$appendsarray[]Additional data untuk merge ke response Lihat selengkapnya
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options
$content

Union type parameter yang accept berbagai tipe data dan auto-detect appropriate response format.

Accepted Types:

  • Illuminate\Support\Collection -> Calls returnJson() internally
  • Illuminate\Database\Eloquent\Collection -> Calls returnJson() internally
  • Illuminate\Pagination\LengthAwarePaginator -> Calls returnPaginateJson()
  • Illuminate\Pagination\CursorPaginator -> Calls returnCursorPaginateJson()
  • Illuminate\Pagination\Paginator -> Calls returnSimplePaginateJson()
  • array -> Calls returnJson() internally

Contoh:

php
// Array
return $this->returnAdaptive(['item1', 'item2']);

// Collection
return $this->returnAdaptive(collect([...]));

// LengthAware
return $this->returnAdaptive(User::paginate(10));

// Cursor
return $this->returnAdaptive(Post::cursorPaginate(20));

// Simple
return $this->returnAdaptive(Product::simplePaginate(15));
$mapper

Callback function untuk transform setiap item sebelum dikirim ke response.

Signature:

php
callable(mixed $item, ?int $index = null): mixed

Parameters:

  • $item - Current item dari array/Collection
  • $index - Index position (optional, untuk array only)

Contoh:

php
// Simple transformation
return $this->returnAdaptive($users, mapper: function($user) {
    return [
        'id' => $user->id,
        'name' => $user->name,
    ];
});

// With index (array only)
return $this->returnAdaptive($items, mapper: function($item, $index) {
    return [
        'position' => $index + 1,
        'data' => $item,
    ];
});

// Arrow function syntax
return $this->returnAdaptive(
    $products,
    mapper: fn($p) => ['id' => $p->id, 'name' => $p->name]
);

Use Case:

Gunakan mapper untuk presentation-layer transformations: hide sensitive fields, compute derived properties, format dates, include related data.

$appends

Array of additional data untuk merge ke response. Built-in keys tidak bisa di-override.

Structure:

php
[
    'custom_key' => mixed,
    'another_key' => mixed,
]

Protected Keys (Cannot Override):

  • message, content, success, validation
  • max_page, current_page, per_page, total, has_more
  • next_cursor, previous_cursor

Contoh:

php
return $this->returnAdaptive($products, appends: [
    'meta' => [
        'version' => '2.0',
        'timestamp' => now()->toIso8601String(),
    ],
    'filters' => request()->only(['category', 'price_min']),
]);

Use Case:

Gunakan untuk API metadata, filter information, aggregated data, atau debug info (development only).

Returns: JsonResponse

Auto-Detection Logic:

php
if ($content instanceof LengthAwarePaginator) {
    return $this->returnPaginateJson(...);
}

if ($content instanceof CursorPaginator) {
    return $this->returnCursorPaginateJson(...);
}

if ($content instanceof Paginator) {
    return $this->returnSimplePaginateJson(...);
}

// Default: array or Collection
return $this->returnJson(...);

Why Use This Over Specific Methods:

  • ✅ Single method untuk all list types
  • ✅ Flexible untuk changing requirements
  • ✅ Less code duplication
  • ✅ Easier refactoring (change pagination type tanpa change method call)

returnJson()

Base method untuk single data atau error responses dengan optional validation errors.

Use cases:

  • Single resource responses (show, store, update)
  • Error responses (404, 500, 403)
  • Validation errors (422) - exclusive feature
  • Empty success responses

Signature:

php
protected function returnJson(
    mixed $content = [],
    int $code = 200,
    string $message = '',
    array|MessageBag $validationErrors = [],
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentmixed[]Data utama response
$codeint200HTTP status code
$messagestring''Custom message atau default
$validationErrorsarray|MessageBag[]Validation errors Lihat selengkapnya
$mapper?callablenullTransform callback Lihat selengkapnya
$appendsarray[]Additional response data Lihat selengkapnya
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options
$validationErrors

Validation errors dari Laravel Validator. Hanya muncul di response jika $code == 422.

Signature:

php
array|Illuminate\Support\MessageBag $validationErrors

Accepted Formats:

php
// MessageBag (from Validator)
$validator = Validator::make(...);
return $this->returnJson([], 422, validationErrors: $validator->errors());

// Array format
return $this->returnJson([], 422, validationErrors: [
    'email' => ['Email sudah digunakan'],
    'password' => ['Password minimal 8 karakter'],
]);

Contoh:

php
public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8',
    ]);
    
    if ($validator->fails()) {
        return $this->returnJson(
            content: [],
            code: 422,
            message: 'Validasi gagal',
            validationErrors: $validator->errors()
        );
    }
    
    // ... create user
}

Output:

json
{
  "message": "Validasi gagal",
  "content": [],
  "success": false,
  "validation": {
    "email": ["The email has already been taken."],
    "password": ["The password must be at least 8 characters."]
  }
}

Use Case:

Exclusive untuk validation error responses (422). Field validation tidak muncul untuk HTTP codes lain.

$mapper

Same as returnAdaptive() - see $mapper section above.

$appends

Same as returnAdaptive() - see $appends section above.

Returns: JsonResponse

Response Structure:

php
[
    'message' => string,
    'content' => mixed,
    'success' => bool,              // true if 200-299
    'validation' => array|null,     // only if code == 422
]

Validation Errors Feature:

Unique feature yang hanya tersedia di returnJson():

  • Parameter $validationErrors accept MessageBag atau array
  • Field validation hanya muncul jika $code == 422
  • Other pagination methods tidak support validation errors

returnPaginateJson()

Explicit method untuk LengthAware pagination responses.

Use cases:

  • API contract requires page-based pagination dengan total count
  • Admin dashboards butuh total pages untuk UI controls
  • Performance critical - skip auto-detection
  • Explicit code clarity preferred

Signature:

php
protected function returnPaginateJson(
    LengthAwarePaginator $content,
    int $code = 200,
    string $message = 'Success',
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentLengthAwarePaginatorrequiredLengthAware paginated data
$codeint200HTTP status code
$messagestring'Success'Custom message atau default
$mapper?callablenullTransform callback Lihat selengkapnya
$appendsarray[]Additional response data Lihat selengkapnya
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options
$mapper

Same pattern as other methods - see $mapper section.

$appends

Same pattern as other methods - see $appends section.

Returns: JsonResponse

Response Structure:

php
[
    'message' => string,
    'content' => array,
    'success' => bool,
    'max_page' => int,
    'current_page' => int,
    'per_page' => int,
    'total' => int,
    'has_more' => bool,
    'next_cursor' => null,        // Always null (unused)
    'previous_cursor' => null,    // Always null (unused)
]

When to Use This Directly:

  • ✅ Frontend needs total count dan max pages
  • ✅ Building pagination UI dengan numbered pages
  • ✅ API documentation specifies LengthAware pagination
  • ✅ Performance: avoid type checking overhead

Advantages Over returnAdaptive():

  • Type-safe: guarantees LengthAwarePaginator input
  • No runtime type detection
  • Clear intent di code

returnCursorPaginateJson()

Explicit method untuk Cursor pagination responses (infinite scroll).

Use cases:

  • Infinite scroll implementations
  • Real-time feeds (social media, news)
  • Large datasets tanpa total count overhead
  • Performance: stateless pagination

Signature:

php
protected function returnCursorPaginateJson(
    CursorPaginator $content,
    int $code = 200,
    string $message = 'Success',
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentCursorPaginatorrequiredCursor paginated data
$codeint200HTTP status code
$messagestring'Success'Custom message atau default
$mapper?callablenullTransform callback Lihat selengkapnya
$appendsarray[]Additional response data Lihat selengkapnya
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options
$mapper

Same pattern as other methods - see $mapper section.

$appends

Same pattern as other methods - see $appends section.

Returns: JsonResponse

Response Structure:

php
[
    'message' => string,
    'content' => array,
    'success' => bool,
    'per_page' => int,
    'has_more' => bool,
    'next_cursor' => ?string,
    'previous_cursor' => ?string,
    'current_page' => null,    // Always null (unused)
    'max_page' => null,        // Always null (unused)
    'total' => null,           // Always null (unused)
]

Cursor Pagination Specifics:

  • next_cursor: Encoded cursor untuk next page (null jika no more pages)
  • previous_cursor: Encoded cursor untuk previous page (null jika first page)
  • No total, max_page, current_page - stateless pagination

When to Use This Directly:

  • ✅ Infinite scroll UI pattern
  • ✅ Real-time feeds dengan constantly changing data
  • ✅ Large datasets (millions of rows) - better performance than LengthAware
  • ✅ API contract specifies cursor-based pagination

returnSimplePaginateJson()

Explicit method untuk Simple pagination responses (lightweight, no total count).

Use cases:

  • Simple pagination tanpa total count overhead
  • "Next/Previous" navigation only
  • Lightweight APIs untuk mobile
  • Don't need total pages information

Signature:

php
protected function returnSimplePaginateJson(
    Paginator $content,
    int $code = 200,
    string $message = 'Success',
    ?callable $mapper = null,
    array $appends = [],
    array $returnHeaders = [],
    int $returnOptions = JSON_THROW_ON_ERROR
): JsonResponse

Parameters

NameTypeDefaultDescription
$contentPaginatorrequiredSimple paginated data
$codeint200HTTP status code
$messagestring'Success'Custom message atau default
$mapper?callablenullTransform callback Lihat selengkapnya
$appendsarray[]Additional response data Lihat selengkapnya
$returnHeadersarray[]Custom HTTP headers
$returnOptionsintJSON_THROW_ON_ERRORJSON encoding options
$mapper

Same pattern as other methods - see $mapper section.

$appends

Same pattern as other methods - see $appends section.

Returns: JsonResponse

Response Structure:

php
[
    'message' => string,
    'content' => array,
    'success' => bool,
    'per_page' => int,
    'current_page' => int,
    'has_more' => bool,
    'total' => null,           // Always null (unused)
    'max_page' => null,        // Always null (unused)
    'next_cursor' => null,     // Always null (unused)
    'previous_cursor' => null, // Always null (unused)
]

Simple Pagination Specifics:

  • Lightweight: doesn't query total count
  • Only knows has_more (next page exists atau tidak)
  • No total atau max_page - faster queries
  • Best untuk mobile apps atau simple listings

When to Use This Directly:

  • ✅ Simple "Next/Previous" navigation
  • ✅ Don't need total count information
  • ✅ Performance optimization (avoid COUNT query)
  • ✅ Mobile-first APIs dengan limited data needs

Examples

Contains:


1. Error Responses (404, 422, 500)

Berbagai error response patterns dengan HTTP codes berbeda.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use App\Models\Product;

class ProductController extends Controller
{
    use JsonResponseTrait;
    
    // 404 Not Found
    public function show($id)
    {
        $product = Product::find($id);
        
        if (!$product) {
            return $this->returnJson([], 404, 'Product tidak ditemukan');
        }
        
        return $this->returnJson($product);
    }
    
    // 422 Validation Error (Exclusive Feature)
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|min:3',
            'price' => 'required|numeric|min:0',
            'category_id' => 'required|exists:categories,id',
        ]);
        
        if ($validator->fails()) {
            return $this->returnJson(
                content: [],
                code: 422,
                message: 'Data produk tidak valid',
                validationErrors: $validator->errors()
            );
        }
        
        $product = Product::create($request->all());
        return $this->returnJson($product, 201, 'Product berhasil dibuat');
    }
    
    // 500 Server Error
    public function process($id)
    {
        try {
            $product = Product::findOrFail($id);
            
            // Complex processing that might fail
            $result = app('SomeComplexService')->processProduct($product);
            
            return $this->returnJson($result, 200, 'Processing berhasil');
            
        } catch (\Exception $e) {
            Log::error('Product processing failed', [
                'product_id' => $id,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            
            return $this->returnJson(
                content: [],
                code: 500,
                message: 'Terjadi kesalahan saat memproses produk'
            );
        }
    }
    
    // 403 Forbidden
    public function delete($id)
    {
        $product = Product::find($id);
        
        if (!$product) {
            return $this->returnJson([], 404, 'Product tidak ditemukan');
        }
        
        if (!auth()->user()->can('delete', $product)) {
            return $this->returnJson(
                [],
                403,
                'Anda tidak memiliki izin untuk menghapus produk ini'
            );
        }
        
        $product->delete();
        return $this->returnJson([], 200, 'Product berhasil dihapus');
    }
}

Output Examples:

404 Response:

json
{
  "message": "Product tidak ditemukan",
  "content": [],
  "success": false
}

422 Response (Validation):

json
{
  "message": "Data produk tidak valid",
  "content": [],
  "success": false,
  "validation": {
    "name": ["The name field is required."],
    "price": ["The price must be a number.", "The price must be at least 0."]
  }
}

500 Response:

json
{
  "message": "Terjadi kesalahan saat memproses produk",
  "content": [],
  "success": false
}

Key Takeaways:

  • returnJson() untuk semua error responses
  • Validation errors exclusive feature (422 only)
  • Custom messages improve UX
  • Log errors untuk debugging (500)
  • Use appropriate HTTP status codes

2. Explicit Pagination Methods

Kapan dan bagaimana menggunakan explicit pagination methods dibanding returnAdaptive().

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
use App\Models\Post;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    use JsonResponseTrait;
    
    /**
     * LengthAware Pagination - When you need total count
     * 
     * Use case: Admin dashboards, reports, numbered pagination UI
     */
    public function indexWithTotal()
    {
        $products = Product::with('category')
            ->where('status', 'active')
            ->paginate(15);
        
        // Explicit: Type-safe, no auto-detection overhead
        return $this->returnPaginateJson(
            $products,
            mapper: fn($p) => [
                'id' => $p->id,
                'name' => $p->name,
                'price' => $p->price,
                'category' => $p->category->name,
            ]
        );
    }
    
    /**
     * Cursor Pagination - For infinite scroll
     * 
     * Use case: Social feeds, real-time data, large datasets
     */
    public function indexInfiniteScroll(Request $request)
    {
        $posts = Post::with('author')
            ->latest()
            ->cursorPaginate(20);
        
        // Explicit: Clear intent for cursor-based pagination
        return $this->returnCursorPaginateJson(
            $posts,
            mapper: fn($p) => [
                'id' => $p->id,
                'title' => $p->title,
                'excerpt' => $p->excerpt,
                'author' => $p->author->name,
                'published' => $p->created_at->diffForHumans(),
            ]
        );
    }
    
    /**
     * Simple Pagination - Lightweight, no total count
     * 
     * Use case: Mobile apps, simple listings, performance optimization
     */
    public function indexSimple()
    {
        $products = Product::select('id', 'name', 'price')
            ->where('featured', true)
            ->simplePaginate(15);
        
        // Explicit: Lightweight, skip COUNT query
        return $this->returnSimplePaginateJson($products);
    }
    
    /**
     * Comparison: Adaptive vs Explicit
     */
    public function indexAdaptive()
    {
        // Option 1: Adaptive (Flexible)
        $products = Product::paginate(15);
        return $this->returnAdaptive($products);
        // Auto-detects: calls returnPaginateJson() internally
        
        // Option 2: Explicit (Type-safe)
        // return $this->returnPaginateJson($products);
        // Direct call, no type checking
    }
}

Response Structures Comparison:

LengthAware (returnPaginateJson):

json
{
  "message": "Sukses",
  "content": [
    { "id": 1, "name": "Product 1", "price": 100000 }
  ],
  "success": true,
  "max_page": 10,
  "current_page": 1,
  "per_page": 15,
  "total": 150,
  "has_more": true,
  "next_cursor": null,
  "previous_cursor": null
}

Cursor (returnCursorPaginateJson):

json
{
  "message": "Sukses",
  "content": [
    { "id": 1, "title": "Post Title" }
  ],
  "success": true,
  "per_page": 20,
  "has_more": true,
  "next_cursor": "eyJpZCI6MjAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
  "previous_cursor": null,
  "current_page": null,
  "max_page": null,
  "total": null
}

Simple (returnSimplePaginateJson):

json
{
  "message": "Sukses",
  "content": [
    { "id": 1, "name": "Product 1" }
  ],
  "success": true,
  "per_page": 15,
  "current_page": 1,
  "has_more": true,
  "total": null,
  "max_page": null,
  "next_cursor": null,
  "previous_cursor": null
}

When to Use Explicit Methods:

MethodUse WhenBenefits
returnPaginateJson()Need total count, numbered pagesFull pagination metadata
returnCursorPaginateJson()Infinite scroll, real-time feedsStateless, better performance
returnSimplePaginateJson()Simple navigation, mobile appsLightweight, no COUNT query
returnAdaptive()Default, flexible, prototypingAuto-detect, single method

Key Takeaways:

  • Explicit methods provide type safety dan clear intent
  • Use when API contract requires specific pagination type
  • Performance benefit: skip type detection overhead
  • Adaptive method remains default untuk flexibility

3. Custom Headers & Appends

Advanced usage: Custom HTTP headers dan additional response data.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class ProductController extends Controller
{
    use JsonResponseTrait;
    
    /**
     * Example: API with metadata, filters, and custom headers
     */
    public function index(Request $request)
    {
        $query = Product::query();
        
        // Apply filters
        if ($request->has('category')) {
            $query->where('category_id', $request->category);
        }
        
        if ($request->has('min_price')) {
            $query->where('price', '>=', $request->min_price);
        }
        
        if ($request->has('max_price')) {
            $query->where('price', '<=', $request->max_price);
        }
        
        $products = $query->paginate(15);
        
        // Build appends data
        $appends = [
            'meta' => [
                'api_version' => '2.0',
                'timestamp' => now()->toIso8601String(),
                'server_time' => now()->timestamp,
            ],
            'applied_filters' => $request->only(['category', 'min_price', 'max_price']),
            'aggregations' => [
                'total_value' => $products->sum('price'),
                'average_price' => $products->avg('price'),
                'min_price' => $products->min('price'),
                'max_price' => $products->max('price'),
            ],
        ];
        
        // Add debug info in development
        if (app()->environment('local')) {
            $appends['debug'] = [
                'query_count' => count(DB::getQueryLog()),
                'memory_usage' => memory_get_peak_usage(true) / 1024 / 1024 . ' MB',
            ];
        }
        
        return $this->returnAdaptive(
            content: $products,
            mapper: fn($p) => [
                'id' => $p->id,
                'name' => $p->name,
                'price' => $p->price,
            ],
            appends: $appends,
            returnHeaders: [
                'X-API-Version' => '2.0',
                'X-RateLimit-Limit' => '100',
                'X-RateLimit-Remaining' => '95',
                'X-Total-Count' => $products->total(),
            ]
        );
    }
    
    /**
     * Example: Attempting to override built-in keys (won't work)
     */
    public function testProtection()
    {
        $products = Product::take(3)->get();
        
        return $this->returnAdaptive(
            content: $products,
            appends: [
                // ❌ These will be IGNORED (protected keys)
                'message' => 'This will not override',
                'success' => false,
                'content' => ['fake data'],
                
                // ✅ These will be ADDED (custom keys)
                'custom_field' => 'This will appear',
                'meta' => ['info' => 'This works'],
            ]
        );
    }
}

Output Example:

json
{
  "message": "Sukses",
  "content": [
    { "id": 1, "name": "Product A", "price": 100000 },
    { "id": 2, "name": "Product B", "price": 150000 }
  ],
  "success": true,
  "max_page": 5,
  "current_page": 1,
  "per_page": 15,
  "total": 75,
  "has_more": true,
  "meta": {
    "api_version": "2.0",
    "timestamp": "2024-12-26T10:30:00+00:00",
    "server_time": 1703588400
  },
  "applied_filters": {
    "category": "electronics",
    "min_price": "50000",
    "max_price": "200000"
  },
  "aggregations": {
    "total_value": 1500000,
    "average_price": 125000,
    "min_price": 50000,
    "max_price": 200000
  },
  "debug": {
    "query_count": 3,
    "memory_usage": "12.5 MB"
  }
}

Response Headers:

X-API-Version: 2.0
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-Total-Count: 75
Content-Type: application/json

Key Takeaways:

  • appends tidak bisa override built-in keys (message, content, success, pagination metadata)
  • returnHeaders untuk custom HTTP headers (API versioning, rate limiting, etc.)
  • Useful untuk:
    • API metadata dan versioning
    • Applied filters information
    • Aggregated statistics
    • Debug information (development only)
    • Rate limiting headers
  • Protection mechanism ensures response structure consistency

4. Comparison - Adaptive vs Explicit

Side-by-side comparison kapan menggunakan returnAdaptive() vs explicit pagination methods.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;
use Illuminate\Http\Request;

class ComparisonController extends Controller
{
    use JsonResponseTrait;
    
    /**
     * Scenario 1: Default Case - Use Adaptive
     * 
     * Best for: General CRUD endpoints, flexible requirements
     */
    public function scenarioDefault()
    {
        $products = Product::paginate(15);
        
        // ✅ RECOMMENDED: Adaptive
        return $this->returnAdaptive($products);
        // - Auto-detects LengthAwarePaginator
        // - Calls returnPaginateJson() internally
        // - Flexible if pagination type changes later
    }
    
    /**
     * Scenario 2: Dynamic Pagination - Use Adaptive
     * 
     * Best for: Query might return different types based on conditions
     */
    public function scenarioDynamic(Request $request)
    {
        $query = Product::query();
        
        // Pagination type depends on request
        if ($request->boolean('all')) {
            // Return all items (Collection)
            $products = $query->get();
        } elseif ($request->boolean('cursor')) {
            // Cursor pagination
            $products = $query->cursorPaginate(20);
        } else {
            // Default: LengthAware pagination
            $products = $query->paginate(15);
        }
        
        // ✅ RECOMMENDED: Adaptive handles all cases
        return $this->returnAdaptive($products);
    }
    
    /**
     * Scenario 3: Strict API Contract - Use Explicit
     * 
     * Best for: Documented APIs (OpenAPI/Swagger), strict typing
     */
    public function scenarioStrictContract()
    {
        // API documentation guarantees LengthAwarePaginator
        $products = Product::paginate(15);
        
        // ✅ RECOMMENDED: Explicit for strict contracts
        return $this->returnPaginateJson($products);
        // - Type-safe: ensures LengthAwarePaginator
        // - Clear intent in code
        // - Matches API documentation
    }
    
    /**
     * Scenario 4: Performance Critical - Use Explicit
     * 
     * Best for: High traffic endpoints, micro-optimization needed
     */
    public function scenarioPerformance()
    {
        $products = Product::cursorPaginate(20);
        
        // ✅ RECOMMENDED: Explicit for performance
        return $this->returnCursorPaginateJson($products);
        // - No type checking overhead
        // - Direct method call
        // - Measurable improvement at scale
    }
    
    /**
     * Scenario 5: Team Convention - Depends
     * 
     * Follow team standards
     */
    public function scenarioTeamConvention()
    {
        $products = Product::paginate(15);
        
        // Team prefers explicit typing
        return $this->returnPaginateJson($products);
        
        // OR: Team prefers flexibility
        // return $this->returnAdaptive($products);
    }
    
    /**
     * Comparison: Both produce identical output
     */
    public function comparison()
    {
        $products = Product::paginate(15);
        
        // Approach 1: Adaptive (Flexible)
        $response1 = $this->returnAdaptive($products);
        
        // Approach 2: Explicit (Type-safe)
        $response2 = $this->returnPaginateJson($products);
        
        // Both produce IDENTICAL JSON output
        // Difference is in type checking and intent
        
        return $response1; // or $response2 - same result
    }
}

Identical Output (Both Methods):

json
{
  "message": "Sukses",
  "content": [
    { "id": 1, "name": "Product 1" },
    { "id": 2, "name": "Product 2" }
  ],
  "success": true,
  "max_page": 10,
  "current_page": 1,
  "per_page": 15,
  "total": 150,
  "has_more": true,
  "next_cursor": null,
  "previous_cursor": null
}

Decision Matrix:

FactorUse returnAdaptive()Use Explicit Method
Flexibility✅ High - handles any type❌ Low - locked to one type
Type Safety⚠️ Runtime detection✅ Compile-time guarantee
Performance⚠️ Tiny overhead✅ Direct call, no overhead
Code Clarity⚠️ Intent less clear✅ Clear intent
Refactoring✅ Easy - change query only⚠️ Must change method call
API Contract⚠️ Flexible (pro/con)✅ Strict contract
Best ForPrototyping, CRUD, dynamicProduction, documented APIs

Code Comparison:

php
// ✅ Adaptive: One method, handles all
public function index(Request $request)
{
    $products = $request->boolean('all')
        ? Product::all()
        : Product::paginate(15);
    
    return $this->returnAdaptive($products); // Works for both
}

// ⚠️ Explicit: Must know type ahead
public function index(Request $request)
{
    if ($request->boolean('all')) {
        $products = Product::all();
        return $this->returnJson($products); // Different method
    } else {
        $products = Product::paginate(15);
        return $this->returnPaginateJson($products); // Different method
    }
}

Performance Benchmark (Hypothetical):

returnAdaptive():        0.15ms (includes type checking)
returnPaginateJson():    0.12ms (direct call)
Difference:              0.03ms (negligible untuk most apps)

At 10,000 req/s:        300ms total overhead per second

Recommendation:

  • Default: Use returnAdaptive() untuk flexibility
  • Switch to explicit when:
    • API contract is documented dan fixed
    • Performance profiling shows bottleneck
    • Team convention prefers explicit typing
    • Large-scale production APIs

Key Takeaways:

  • Both methods produce identical output
  • Adaptive = flexibility, Explicit = type safety
  • Performance difference negligible untuk most cases
  • Choose based on project needs dan team convention
  • You can mix both approaches di different endpoints

Laravel Integration

Using in Controllers

Trait dirancang untuk digunakan di Laravel controllers. Import dan gunakan di base controller atau individual controllers.

Base Controller Pattern (Recommended):

php
<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Bpmlib\Laresponse\Traits\JsonResponseTrait;

abstract class Controller extends BaseController
{
    use AuthorizesRequests, ValidatesRequests;
    use JsonResponseTrait; // Available to all controllers
}

Setelah setup di base controller, semua controllers otomatis punya access:

php
<?php

namespace App\Http\Controllers;

use App\Models\User;

class UserController extends Controller
{
    // JsonResponseTrait methods available here
    
    public function index()
    {
        return $this->returnAdaptive(User::paginate(15));
    }
    
    public function show($id)
    {
        $user = User::find($id);
        
        if (!$user) {
            return $this->returnJson([], 404);
        }
        
        return $this->returnJson($user);
    }
}

Individual Controller Pattern:

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\Product;

class ProductController extends Controller
{
    use JsonResponseTrait; // Only this controller has access
    
    public function index()
    {
        return $this->returnAdaptive(Product::paginate(10));
    }
}

Using in API Resources

Combine dengan Laravel API Resources untuk complex data transformations.

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use App\Models\User;
use App\Http\Resources\UserResource;

class UserController extends Controller
{
    use JsonResponseTrait;
    
    /**
     * Option 1: API Resource + returnAdaptive
     */
    public function index()
    {
        $users = User::with('posts', 'profile')->paginate(15);
        
        return $this->returnAdaptive(
            $users,
            mapper: fn($user) => new UserResource($user)
        );
    }
    
    /**
     * Option 2: API Resource collection directly
     */
    public function indexWithCollection()
    {
        $users = User::paginate(15);
        
        // Transform with Resource, then wrap in returnAdaptive
        $transformed = UserResource::collection($users);
        
        return $this->returnAdaptive($transformed);
    }
    
    /**
     * Option 3: Manual mapping (simple cases)
     */
    public function indexSimple()
    {
        return $this->returnAdaptive(
            User::paginate(15),
            mapper: fn($u) => [
                'id' => $u->id,
                'name' => $u->name,
                'email' => $u->email,
            ]
        );
    }
}

UserResource Example:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'posts_count' => $this->posts_count,
            'profile' => [
                'avatar' => $this->profile?->avatar_url,
                'bio' => $this->profile?->bio,
            ],
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}

Best Practices

Error Handling Pattern

Consistent error handling across application:

php
<?php

namespace App\Http\Controllers;

use Bpmlib\Laresponse\Traits\JsonResponseTrait;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;

class BaseApiController extends Controller
{
    use JsonResponseTrait;
    
    /**
     * Handle common exceptions
     */
    protected function handleException(\Exception $e)
    {
        if ($e instanceof ModelNotFoundException) {
            return $this->returnJson([], 404, 'Resource not found');
        }
        
        if ($e instanceof ValidationException) {
            return $this->returnJson(
                [],
                422,
                'Validation failed',
                validationErrors: $e->errors()
            );
        }
        
        // Default: 500 error
        return $this->returnJson([], 500, 'Internal server error');
    }
}

Usage:

php
public function show($id)
{
    try {
        $user = User::findOrFail($id);
        return $this->returnJson($user);
    } catch (\Exception $e) {
        return $this->handleException($e);
    }
}

Consistent Response Pattern Across API

Establish team conventions:

php
/**
 * Team Convention Example
 */

// ✅ DO: Use returnAdaptive for lists
public function index()
{
    return $this->returnAdaptive(Product::paginate(15));
}

// ✅ DO: Use returnJson for single resources
public function show($id)
{
    return $this->returnJson(Product::find($id));
}

// ✅ DO: Use returnJson for errors
public function destroy($id)
{
    $product = Product::find($id);
    
    if (!$product) {
        return $this->returnJson([], 404);
    }
    
    $product->delete();
    return $this->returnJson([], 200, 'Deleted successfully');
}

// ✅ DO: Use mapper for transformations
public function index()
{
    return $this->returnAdaptive(
        User::paginate(15),
        mapper: [$this, 'transformUser']
    );
}

private function transformUser($user)
{
    return [
        'id' => $user->id,
        'name' => $user->name,
    ];
}

When to Use Adaptive vs Explicit

Use returnAdaptive() by default:

php
// ✅ General CRUD endpoints
public function index()
{
    return $this->returnAdaptive(Product::paginate(15));
}

// ✅ Dynamic queries
public function search(Request $request)
{
    $query = Product::query();
    
    if ($request->has('filter')) {
        $query->where(...);
    }
    
    $results = $request->boolean('paginate')
        ? $query->paginate(15)
        : $query->get();
    
    return $this->returnAdaptive($results);
}

Use explicit methods when:

php
// ✅ Documented API contracts
/**
 * @OA\Get(
 *   path="/api/products",
 *   @OA\Response(response=200, description="LengthAware pagination")
 * )
 */
public function index()
{
    return $this->returnPaginateJson(Product::paginate(15));
}

// ✅ Performance critical (high traffic)
public function feed()
{
    // Direct call, no type checking
    return $this->returnCursorPaginateJson(
        Post::latest()->cursorPaginate(20)
    );
}

// ✅ Team convention prefers explicit
public function index()
{
    // Clear, explicit, type-safe
    return $this->returnPaginateJson(User::paginate(10));
}