Skip to main content
Relation loading is one of the most powerful features in Rest Generic Class. This guide shows you how to efficiently load related data, select specific fields, and combine relation loading with filtering.

Relation Configuration

Before you can load relations through the API, they must be declared in your model’s RELATIONS constant.

Security Whitelist

The RELATIONS constant acts as a security whitelist:
Product.php
class Product extends BaseModel
{
    const RELATIONS = ['category', 'reviews', 'supplier'];

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function reviews()
    {
        return $this->hasMany(Review::class);
    }

    public function supplier()
    {
        return $this->belongsTo(Supplier::class);
    }
}
Only relations listed in RELATIONS can be loaded via the API. Requests for unlisted relations will return a 400 error.

Why Whitelist Relations?

  1. Security: Prevents exposure of sensitive relationships
  2. Performance: Limits accidental eager loading of expensive relations
  3. API Design: Explicitly defines your public API surface

Basic Relation Loading

Load All Fields

Load complete related models:
GET /api/v1/products?relations=["category"]
Response:
{
  "data": [
    {
      "id": 1,
      "name": "Wireless Mouse",
      "price": 29.99,
      "category_id": 3,
      "category": {
        "id": 3,
        "name": "Peripherals",
        "description": "Computer peripherals and accessories",
        "parent_id": null,
        "created_at": "2026-01-01T00:00:00.000000Z",
        "updated_at": "2026-01-01T00:00:00.000000Z"
      }
    }
  ]
}

Load Multiple Relations

GET /api/v1/products?relations=["category","reviews","supplier"]

The “all” Shortcut

Load all allowed relations at once:
GET /api/v1/products?relations=["all"]
This loads every relation in RELATIONS. Use cautiously on models with many relations or large datasets.

Field Selection in Relations

Reduce payload size by selecting only needed fields from relations.

Syntax

Use the colon syntax: "relation:field1,field2,field3"
GET /api/v1/products?relations=["category:id,name"]
Response:
{
  "data": [
    {
      "id": 1,
      "name": "Wireless Mouse",
      "price": 29.99,
      "category_id": 3,
      "category": {
        "id": 3,
        "name": "Peripherals"
      }
    }
  ]
}

Foreign Key Auto-Inclusion

The package automatically includes foreign keys to maintain relationships:
GET /api/v1/products?relations=["category:name"]
Even though only name was requested, the foreign key category_id is automatically included:
{
  "category": {
    "id": 3,
    "name": "Peripherals"
  }
}
Always include the primary key (id) in your selection. It’s required for relationship mapping.

Multiple Relations with Fields

GET /api/v1/products?relations=["category:id,name","supplier:id,name,email"]

Nested Relations

Load relations of relations using dot notation.

Two Levels Deep

GET /api/v1/products?relations=["category.parent"]
Response:
{
  "data": [
    {
      "id": 1,
      "name": "Wireless Mouse",
      "category_id": 3,
      "category": {
        "id": 3,
        "name": "Peripherals",
        "parent_id": 1,
        "parent": {
          "id": 1,
          "name": "Electronics"
        }
      }
    }
  ]
}

Nested with Field Selection

GET /api/v1/products?relations=["category.parent:id,name"]
This selects specific fields from the nested parent relation:
{
  "category": {
    "id": 3,
    "name": "Peripherals",
    "parent_id": 1,
    "parent": {
      "id": 1,
      "name": "Electronics"
    }
  }
}

Three Levels Deep

GET /api/v1/orders?relations=["items.product.category:id,name"]
Loads: Order → Order Items → Product → Category

Relation Validation

The package validates all relation paths to prevent typos and unauthorized access.

Invalid Relation Error

Request:
GET /api/v1/products?relations=["invalid_relation"]
Response (400 Bad Request):
{
  "message": "Relation 'invalid_relation' is not allowed. Allowed: category, reviews, supplier"
}

Strict Validation Mode

By default, models must explicitly define RELATIONS:
config/rest-generic-class.php
'filtering' => [
    'strict_relations' => true, // Require explicit RELATIONS declaration
],
With strict_relations=true, models without RELATIONS will throw an error:
{
  "message": "Model App\\Models\\Product must define const RELATIONS for security. Set 'filtering.strict_relations' => false to auto-detect (not recommended)."
}
Never disable strict_relations in production. Auto-detection can expose unintended relations and create security vulnerabilities.

Combining Relations with Filtering

Relation loading and filtering work together seamlessly.

Load Relations on Filtered Results

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

{
  "oper": {
    "and": [
      "status|=|active",
      "price|>=|50"
    ]
  }
}
This:
  1. Filters products (status=active AND price>=50)
  2. Eager-loads category and supplier for matching products

Filter by Relation Properties

Filter the root query based on related data:
GET /api/v1/products?relations=["category:id,name"]
Content-Type: application/json

{
  "oper": {
    "and": ["status|=|active"],
    "category": {
      "and": ["name|like|%electronics%"]
    }
  }
}
This returns products:
  1. With status=active
  2. That belong to a category with “electronics” in the name
  3. With the category data eager-loaded

Filtering Eager-Loaded Relations

Use _nested=true to filter the relations themselves, not just the root query.

Without _nested (Default)

GET /api/v1/products?relations=["reviews"]
Content-Type: application/json

{
  "oper": {
    "reviews": {
      "and": ["rating|>=|4"]
    }
  }
}
Result:
  • Returns products that have reviews with rating >= 4
  • Loads all reviews for those products (including ratings < 4)

With _nested=true

GET /api/v1/products?relations=["reviews"]
Content-Type: application/json

{
  "_nested": true,
  "oper": {
    "reviews": {
      "and": ["rating|>=|4"]
    }
  }
}
Result:
  • Returns products that have reviews with rating >= 4
  • Loads only reviews with rating >= 4
With _nested=true, the eager-loaded reviews array will only contain filtered items. This changes your response structure.

Optimizing N+1 Queries

N+1 queries occur when you load a list of records, then query for related data inside a loop.

The Problem

Without eager loading:
// 1 query: get all products
$products = Product::where('status', 'active')->get();

foreach ($products as $product) {
    // N queries: one per product!
    echo $product->category->name;
}

// Total: 1 + N queries

The Solution

With eager loading:
GET /api/v1/products?relations=["category:id,name"]
Content-Type: application/json

{
  "oper": {"and": ["status|=|active"]}
}
The package automatically eager-loads relations:
// 1 query: get all products
// 1 query: get all related categories
$products = Product::where('status', 'active')
    ->with(['category:id,name'])
    ->get();

// Total: 2 queries regardless of N

Monitoring Query Count

Enable query logging to verify eager loading:
.env
LOG_QUERY=true
Check storage/logs/rest-generic-class.log for executed queries.

Polymorphic Relations

Rest Generic Class supports polymorphic relations with proper configuration.

Setup

Comment.php
class Comment extends BaseModel
{
    const RELATIONS = ['commentable'];

    public function commentable()
    {
        return $this->morphTo();
    }
}
Product.php
class Product extends BaseModel
{
    const RELATIONS = ['comments'];

    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Loading Polymorphic Relations

GET /api/v1/comments?relations=["commentable"]
Response:
{
  "data": [
    {
      "id": 1,
      "body": "Great product!",
      "commentable_type": "App\\Models\\Product",
      "commentable_id": 10,
      "commentable": {
        "id": 10,
        "name": "Wireless Mouse",
        "price": 29.99
      }
    }
  ]
}

Real-World Examples

E-commerce: Product Listing with Category and Reviews

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

{
  "oper": {
    "and": [
      "status|=|active",
      "stock|>|0"
    ]
  },
  "orderby": [{"name": "asc"}],
  "pagination": {"page": 1, "pageSize": 20}
}

CRM: Leads with Assigned User and Company

GET /api/v1/leads?relations=["assignedUser:id,name,email","company:id,name,industry"]
Content-Type: application/json

{
  "oper": {
    "and": [
      "status|=|qualified",
      "estimated_value|>=|10000"
    ],
    "assignedUser": {
      "and": ["active|=|true"]
    }
  },
  "orderby": [{"estimated_value": "desc"}]
}

Blog: Posts with Author, Category, and Comment Count

GET /api/v1/posts?relations=["author:id,name,avatar_url","category:id,name,slug"]
Content-Type: application/json

{
  "oper": {
    "and": [
      "published|=|true",
      "published_at|<=|2026-03-05"
    ]
  },
  "orderby": [{"published_at": "desc"}]
}

Troubleshooting

Relation Not Loaded

Symptom: Relation is null or missing from response Causes:
  • Relation not in RELATIONS constant
  • Typo in relation name
  • Foreign key is null
Solution:
  • Verify: php artisan tinkerProduct::RELATIONS
  • Check raw data: GET /api/v1/products (without relations param)
  • Look for null foreign keys

Wrong Fields in Relation

Symptom: Relation includes fields you didn’t select Cause: Foreign key auto-inclusion Explanation: The package adds foreign keys automatically to maintain relationships. This is intentional.

Performance Issues

Symptom: Slow response times with relations Causes:
  • Loading too many relations
  • Large collections without pagination
  • Missing database indexes on foreign keys
Solutions:
  • Load only needed fields: category:id,name
  • Always paginate large datasets
  • Add indexes: $table->index('category_id')

Next Steps

Advanced Filtering

Learn to combine relation loading with complex filters

Hierarchical Data

Work with self-referencing tree structures

Many-to-Many

Manage pivot tables and attach/detach operations

Caching

Cache relation queries for better performance

Evidence

  • File: src/Core/Services/BaseService.php
    Lines: 98-174, 176-224
    Implements relations() method with field selection, validation, and _nested support
  • File: src/Core/Services/BaseService.php
    Lines: 1334-1395, 1397-1438
    Shows getRelationsForModel() and extractRelationFiltersForModel() for validation
  • File: src/Core/Models/BaseModel.php
    Lines: 40-44
    Defines RELATIONS constant used for whitelisting

Build docs developers (and LLMs) love