Skip to main content
Bulk operations allow you to create, update, or delete multiple records in a single request. Rest Generic Class provides built-in bulk support with automatic transaction handling and rollback on errors.

Why Bulk Operations?

Without bulk operations:
// 100 separate requests
for (const product of products) {
  await fetch(`/api/v1/products/${product.id}`, {
    method: 'PUT',
    body: JSON.stringify({ stock: product.stock })
  });
}
// Total time: ~5 seconds
// Network overhead: 100 round trips
With bulk operations:
// 1 request
await fetch('/api/v1/products/update-multiple', {
  method: 'POST',
  body: JSON.stringify({
    product: products.map(p => ({ id: p.id, stock: p.stock }))
  })
});
// Total time: ~200ms
// Network overhead: 1 round trip
Performance gain: 25x faster

Bulk Create

Setup

Define the MODEL constant in your model:
Product.php
class Product extends BaseModel
{
    const MODEL = 'product'; // Must be lowercase
    
    protected $fillable = ['name', 'price', 'stock', 'category_id'];
}

Single Create (Normal)

POST /api/v1/products
Content-Type: application/json

{
  "name": "Wireless Mouse",
  "price": 29.99,
  "stock": 150,
  "category_id": 3
}
Response:
{
  "success": true,
  "model": {
    "id": 10,
    "name": "Wireless Mouse",
    "price": 29.99,
    "stock": 150,
    "category_id": 3
  }
}

Bulk Create

Wrap records in an array with the model key:
POST /api/v1/products
Content-Type: application/json

{
  "product": [
    {
      "name": "Wireless Mouse",
      "price": 29.99,
      "stock": 150,
      "category_id": 3
    },
    {
      "name": "USB-C Cable",
      "price": 12.99,
      "stock": 500,
      "category_id": 4
    },
    {
      "name": "Laptop Stand",
      "price": 39.99,
      "stock": 75,
      "category_id": 3
    }
  ]
}
Response:
{
  "success": true,
  "0": {
    "success": true,
    "model": {
      "id": 10,
      "name": "Wireless Mouse",
      ...
    }
  },
  "1": {
    "success": true,
    "model": {
      "id": 11,
      "name": "USB-C Cable",
      ...
    }
  },
  "2": {
    "success": true,
    "model": {
      "id": 12,
      "name": "Laptop Stand",
      ...
    }
  }
}
The key must match the lowercase MODEL constant. For Product::MODEL = 'product', use {"product": [...]}.

Bulk Update

Route Setup

Register the updateMultiple route:
routes/api.php
Route::post('products/update-multiple', [ProductController::class, 'updateMultiple']);

Request Format

Each record must include the primary key:
POST /api/v1/products/update-multiple
Content-Type: application/json

{
  "product": [
    {"id": 10, "stock": 140},
    {"id": 11, "stock": 495},
    {"id": 12, "price": 34.99, "stock": 70}
  ]
}
Response:
{
  "success": true,
  "models": [
    {
      "success": true,
      "model": {
        "id": 10,
        "name": "Wireless Mouse",
        "stock": 140,
        "updated_at": "2026-03-05T12:00:00.000000Z"
      }
    },
    {
      "success": true,
      "model": {
        "id": 11,
        "name": "USB-C Cable",
        "stock": 495,
        "updated_at": "2026-03-05T12:00:00.000000Z"
      }
    },
    {
      "success": true,
      "model": {
        "id": 12,
        "name": "Laptop Stand",
        "price": 34.99,
        "stock": 70,
        "updated_at": "2026-03-05T12:00:00.000000Z"
      }
    }
  ]
}

Partial Updates

You don’t need to send all fields, only the ones you want to update:
{
  "product": [
    {"id": 10, "stock": 140},
    {"id": 11, "stock": 495}
  ]
}

Transaction Handling

All bulk operations are wrapped in database transactions.

Automatic Rollback

If any record fails, the entire transaction rolls back:
POST /api/v1/products/update-multiple
Content-Type: application/json

{
  "product": [
    {"id": 10, "stock": 140},
    {"id": 999, "stock": 0},  // ID doesn't exist
    {"id": 12, "stock": 70}
  ]
}
Response (422 Unprocessable Entity):
{
  "success": false,
  "models": [
    {
      "success": false,
      "errors": {
        "id": ["Product with ID 999 not found"]
      }
    }
  ]
}
Result: ❌ Products 10 and 12 are NOT updated (transaction rolled back)
Bulk operations are all-or-nothing. One failure cancels the entire batch.

Commit on Success

If all records succeed, the transaction commits:
// In RestController::updateMultiple()
DB::beginTransaction();
try {
    $result = $this->service->update_multiple($params);
    if ($result['success'])
        DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();
    throw $e;
}

Validation in Bulk Operations

Per-Record Validation

Each record is validated individually:
POST /api/v1/products
Content-Type: application/json

{
  "product": [
    {"name": "Mouse", "price": 29.99, "stock": 150, "category_id": 3},
    {"name": "Cable", "price": -5.00, "stock": 500, "category_id": 4},  // Invalid price
    {"name": "Stand", "price": 39.99, "stock": 75, "category_id": 3}
  ]
}
Response:
{
  "success": false,
  "1": {
    "success": false,
    "errors": {
      "price": ["The price must be greater than 0"]
    }
  }
}

Validation Rules

Define validation in your model or request:
ProductRequest.php
public function rules(): array
{
    return [
        'name' => 'required|string|max:255',
        'price' => 'required|numeric|min:0',
        'stock' => 'required|integer|min:0',
        'category_id' => 'required|exists:categories,id',
    ];
}
These rules apply to every record in a bulk operation.

Cache Invalidation

Bulk operations invalidate cache for the entire model.

How It Works

// Before bulk update
Product cache version: 5

// Bulk update
POST /api/v1/products/update-multiple
{
  "product": [{"id": 10, "stock": 140}, {"id": 11, "stock": 495}]
}

// After bulk update
Product cache version: 6 (bumped once for entire batch)
Efficiency: Cache version is bumped once per batch, not once per record.
All cached list_all and get_one queries for products are invalidated with a single version bump.

Performance Optimization

Benchmark: 100 Record Updates

MethodRequestsTimeDatabase Queries
Individual PUT1005.2s200+ (2 per record)
Bulk Update10.18s101 (1 + 100 updates)
Improvement: 28.8x faster

Best Practices

Do:
  • Use bulk operations for batches of 10+ records
  • Send only fields that need updating
  • Batch similar operations together
  • Use pagination for very large batches (1000+)
Don’t:
  • Use bulk operations for 1-2 records (overhead not worth it)
  • Send all fields when only one changed
  • Mix creates and updates in same batch
  • Send batches larger than 500 records without testing

Real-World Examples

Inventory Update (E-commerce)

Update stock levels from warehouse system:
POST /api/v1/products/update-multiple
Content-Type: application/json

{
  "product": [
    {"id": 1001, "stock": 50},
    {"id": 1002, "stock": 0},
    {"id": 1003, "stock": 150},
    {"id": 1004, "stock": 75},
    {"id": 1005, "stock": 200}
  ]
}

Price Update (Admin Dashboard)

Apply discount to multiple products:
// Calculate discounted prices
const updates = selectedProducts.map(product => ({
  id: product.id,
  discount_price: product.price * 0.8,  // 20% off
  discount_expires_at: '2026-04-01'
}));

// Send bulk update
await fetch('/api/v1/products/update-multiple', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({ product: updates })
});

Status Batch Update (CRM)

Update lead status after import:
POST /api/v1/leads/update-multiple
Content-Type: application/json

{
  "lead": [
    {"id": 501, "status": "qualified", "assigned_to": 10},
    {"id": 502, "status": "qualified", "assigned_to": 10},
    {"id": 503, "status": "qualified", "assigned_to": 11},
    {"id": 504, "status": "qualified", "assigned_to": 11}
  ]
}

Bulk Create from Import (CSV Upload)

// Parse CSV rows
const products = csvRows.map(row => ({
  name: row.name,
  price: parseFloat(row.price),
  stock: parseInt(row.stock),
  category_id: categoryMap[row.category]
}));

// Chunk into batches of 100
for (let i = 0; i < products.length; i += 100) {
  const batch = products.slice(i, i + 100);
  await fetch('/api/v1/products', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({ product: batch })
  });
}

Error Handling

Partial Success Not Supported

Rest Generic Class does not support partial success in bulk operations. Either all records succeed or all fail. Why?
Partial success leads to inconsistent state and makes error recovery complex.
Alternative:
If you need partial success, send smaller batches:
// Instead of 1 batch of 100 (all-or-nothing)
await bulkUpdate(products); // Fails if any record fails

// Send 10 batches of 10 (isolated failures)
for (const batch of chunks(products, 10)) {
  try {
    await bulkUpdate(batch);
  } catch (error) {
    console.error('Batch failed:', batch, error);
    // Continue with next batch
  }
}

Logging Errors

Bulk operation errors are logged automatically:
// In RestController::updateMultiple()
Log::channel('rest-generic-class')->error('Update multiple failed', [
    'controller' => static::class,
    'method' => __FUNCTION__,
    'exception' => $e,
]);
Check logs at storage/logs/rest-generic-class.log.

Testing Bulk Operations

Unit Test Example

public function test_bulk_update_products()
{
    $products = Product::factory()->count(3)->create();

    $response = $this->postJson('/api/v1/products/update-multiple', [
        'product' => [
            ['id' => $products[0]->id, 'stock' => 100],
            ['id' => $products[1]->id, 'stock' => 200],
            ['id' => $products[2]->id, 'stock' => 300],
        ]
    ]);

    $response->assertStatus(200)
             ->assertJsonPath('success', true)
             ->assertJsonPath('models.0.success', true)
             ->assertJsonPath('models.1.success', true)
             ->assertJsonPath('models.2.success', true);

    $this->assertDatabaseHas('products', ['id' => $products[0]->id, 'stock' => 100]);
    $this->assertDatabaseHas('products', ['id' => $products[1]->id, 'stock' => 200]);
    $this->assertDatabaseHas('products', ['id' => $products[2]->id, 'stock' => 300]);
}

public function test_bulk_update_rollback_on_error()
{
    $product = Product::factory()->create(['stock' => 50]);

    $response = $this->postJson('/api/v1/products/update-multiple', [
        'product' => [
            ['id' => $product->id, 'stock' => 100],
            ['id' => 99999, 'stock' => 200], // Non-existent ID
        ]
    ]);

    $response->assertStatus(422);

    // Original stock should be unchanged (rollback)
    $this->assertDatabaseHas('products', ['id' => $product->id, 'stock' => 50]);
}

Troubleshooting

”MODEL constant not defined” Error

Symptom: Bulk create fails with “MODEL not found” Cause: Missing MODEL constant in model Solution:
const MODEL = 'product'; // Add this to your model

Wrong Key in Request

Symptom: Returns single-record response instead of bulk Cause: Key doesn’t match lowercase MODEL constant Fix:
// Wrong
{"Product": [...]}  // Capital P
{"products": [...]} // Plural

// Correct
{"product": [...]}  // Matches MODEL = 'product'

Transaction Timeout

Symptom: Request times out with large batches Cause: Batch too large or slow validation Solution:
// Increase timeout for bulk endpoints
set_time_limit(300); // 5 minutes

// Or split into smaller batches
$chunks = array_chunk($records, 100);
foreach ($chunks as $chunk) {
    $this->updateMultiple($chunk);
}

Memory Limit Exceeded

Symptom: “Allowed memory size exhausted” Cause: Loading too many models into memory Solution:
// Increase memory limit
ini_set('memory_limit', '256M');

// Or process in smaller batches

Next Steps

Caching

Understand cache invalidation with bulk operations

Many-to-Many

Bulk attach/detach operations for pivot tables

Performance

Advanced performance optimization techniques

Testing

Write integration tests for bulk endpoints

Evidence

  • File: src/Core/Services/BaseService.php
    Lines: 611-627, 664-676
    Implements create() (detects bulk), save_array(), and update_multiple()
  • File: src/Core/Controllers/RestController.php
    Lines: 158-176, 211-230
    Shows store() and updateMultiple() with transaction handling
  • File: src/Core/Models/BaseModel.php
    Lines: 28
    Defines MODEL constant requirement for bulk operations
  • File: README.md
    Lines: 181-194
    Shows bulk update example usage

Build docs developers (and LLMs) love