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:
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:
Route :: post ( 'products/update-multiple' , [ ProductController :: class , 'updateMultiple' ]);
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:
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.
Benchmark: 100 Record Updates
Method Requests Time Database Queries Individual PUT 100 5.2s 200+ (2 per record) Bulk Update 1 0.18s 101 (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