Skip to main content
This page provides a complete, copy-paste-ready CRUD example using the Rest Generic Class package. You’ll build a fully functional Product API with all CRUD operations.

Overview

We’ll create:
  • Model - Product model with relations
  • Service - Product service layer
  • Controller - RESTful API controller
  • Routes - API endpoint definitions
  • HTTP Examples - Complete request/response examples

Step 1: Create the Model

Create app/Models/Product.php:
<?php

namespace App\Models;

use Ronu\RestGenericClass\Core\Models\BaseModel;

class Product extends BaseModel
{
    protected $fillable = [
        'name',
        'description',
        'price',
        'stock',
        'category_id',
        'status'
    ];

    protected $casts = [
        'price' => 'decimal:2',
        'stock' => 'integer',
        'category_id' => 'integer',
    ];

    // Required: model identifier for bulk operations
    const MODEL = 'product';

    // Required: allowlisted relations for security
    const RELATIONS = ['category', 'reviews', 'tags'];

    /**
     * Product belongs to a category
     */
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * Product has many reviews
     */
    public function reviews()
    {
        return $this->hasMany(Review::class);
    }

    /**
     * Product has many tags (many-to-many)
     */
    public function tags()
    {
        return $this->belongsToMany(Tag::class, 'product_tags');
    }
}

Step 2: Create the Service

Create app/Services/ProductService.php:
<?php

namespace App\Services;

use App\Models\Product;
use Ronu\RestGenericClass\Core\Services\BaseService;

class ProductService extends BaseService
{
    public function __construct()
    {
        parent::__construct(Product::class);
    }

    /**
     * Optional: Add custom business logic
     * Example: Validate stock before creating
     */
    public function create($payload)
    {
        // Custom validation
        if (isset($payload['stock']) && $payload['stock'] < 0) {
            throw new \InvalidArgumentException('Stock cannot be negative');
        }

        // Set default status if not provided
        $payload['status'] = $payload['status'] ?? 'active';

        return parent::create($payload);
    }
}

Step 3: Create the Controller

Create app/Http/Controllers/Api/ProductController.php:
<?php

namespace App\Http\Controllers\Api;

use App\Models\Product;
use App\Services\ProductService;
use Ronu\RestGenericClass\Core\Controllers\RestController;

class ProductController extends RestController
{
    protected $modelClass = Product::class;

    public function __construct(ProductService $service)
    {
        $this->service = $service;
    }
}

Step 4: Define Routes

Add to routes/api.php:
use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')->group(function () {
    // Standard REST endpoints
    Route::apiResource('products', ProductController::class);
    
    // Bulk update endpoint
    Route::post('products/update-multiple', [ProductController::class, 'updateMultiple']);
});

Step 5: Database Migration

Create the products table:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->decimal('price', 10, 2);
            $table->integer('stock')->default(0);
            $table->foreignId('category_id')->constrained()->onDelete('cascade');
            $table->enum('status', ['active', 'inactive', 'discontinued'])->default('active');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    public function down()
    {
        Schema::dropIfExists('products');
    }
};

HTTP Request Examples

Create a Product

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

{
  "name": "Wireless Keyboard",
  "description": "Mechanical keyboard with RGB lighting",
  "price": 89.99,
  "stock": 100,
  "category_id": 3,
  "status": "active"
}

List Products with Filtering

GET /api/v1/products?select=["id","name","price"]&relations=["category:id,name"]
Content-Type: application/json

{
  "oper": {
    "and": [
      "status|=|active",
      "price|>=|50",
      "stock|>|0"
    ]
  },
  "orderby": [{"price": "desc"}],
  "pagination": {
    "page": 1,
    "pageSize": 10
  }
}

Get Single Product

GET /api/v1/products/1?relations=["category","reviews:id,rating,comment"]
Content-Type: application/json

Update a Product

PUT /api/v1/products/1
Content-Type: application/json

{
  "price": 79.99,
  "stock": 85
}

Delete a Product

DELETE /api/v1/products/1

Bulk Update Products

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

{
  "product": [
    {"id": 10, "stock": 50, "status": "active"},
    {"id": 11, "stock": 0, "status": "inactive"},
    {"id": 12, "price": 99.99}
  ]
}

Advanced Filtering Examples

Search with Multiple Conditions

{
  "oper": {
    "and": [
      "name|like|%keyboard%",
      "status|=|active",
      {
        "or": [
          "price|<=|100",
          "stock|>|50"
        ]
      }
    ]
  }
}
{
  "oper": {
    "and": ["status|=|active"],
    "category": {
      "and": ["name|like|%electronics%"]
    }
  },
  "relations": ["category:id,name"]
}

Testing Your API

1

Seed Test Data

Create a seeder to populate test products:
php artisan make:seeder ProductSeeder
Product::create([
    'name' => 'Wireless Keyboard',
    'price' => 89.99,
    'stock' => 100,
    'category_id' => 1,
    'status' => 'active'
]);
2

Test with cURL

curl -X GET "http://localhost:8000/api/v1/products" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json"
3

Test with Postman

Import the API endpoints into Postman and test all CRUD operations with various filter combinations.
All endpoints automatically handle validation, error responses, and database exceptions. The BaseService provides built-in caching, filtering, and relation loading out of the box.
Remember to add all relations to the RELATIONS constant in your model. Unlisted relations will be rejected with a 400 error for security reasons.

Next Steps

Build docs developers (and LLMs) love