Courses

How to Build Laravel 12 API From Scratch

List inside of List: Multi-Level Data

Summary of this lesson:
- Creating products table with category relationship
- Handling relationships in API responses
- Using API Resource inside other API Resource
- Managing eager loading with whenLoaded() in the API Resource

In this lesson, we will discuss relationships between tables and between Eloquent Models and how to return them from the API Resources.


First, we must create a new Model to have a relation with a category.

php artisan make:model Product -mf

database/migration/xxx_create_products_table.php:

public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained();
$table->string('name');
$table->text('description');
$table->integer('price');
$table->timestamps();
});
}

app/Models/Product.php:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Product extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
'description',
'price',
];
 
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}

database/factories/ProductFactory.php:

use App\Models\Category;
 
class ProductFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->word(),
'category_id' => Category::inRandomOrder()->first()->id,
'description' => fake()->paragraph(),
'price' => rand(1000, 99999),
];
}
}

database/seeders/DatabaseSeeder.php:

 
use App\Models\Product;
 
class DatabaseSeeder extends Seeder
{
public function run(): void
{
Category::factory(10)->create();
Product::factory(20)->create();
}
}

And run the migrations with the seed.

php artisan migrate:fresh --seed

Next, the Controller, Resource, and Route.

php artisan make:controller Api/ProductController
php artisan make:resource ProductResource

routes/api.php:

Route::get('/user', function (Request $request) {
return $request->user();
})->middleware(Authenticate::using('sanctum'));
 
Route::get('categories', [\App\Http\Controllers\Api\CategoryController::class, 'index']);
Route::get('categories/{category}', [\App\Http\Controllers\Api\CategoryController::class, 'show']);
 
Route::get('products', [\App\Http\Controllers\Api\ProductController::class, 'index']);

In the Controller, we can return the product with category.

app/Http/Controllers/Api/ProductController.php:

use App\Models\Product;
 
class ProductController extends Controller
{
public function index()
{
$products = Product::with('category')->get();
 
return $products;
}
}

After going to /api/products endpoint, we can see all fields from product and category.


As you remember, we can use Resources to hide or modify fields. In this case, we will change the price field. First, let's use the ProductResource in the Controller and add fields in the Resource.

app/Http/Controllers/Api/ProductController.php:

class ProductController extends Controller
{
public function index()
{
$products = Product::with('category')->get();
 
return $products;
return ProductResource::collection($products);
}
}

app/Http/Resources/ProductResource.php:

class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'category_id' => $this->category_id,
'name' => $this->name,
'description' => $this->description,
'price' => number_format($this->price / 100, 2),
'category' => $this->category,
];
}
}

We can see a slightly different result after going to /api/products again. First, we have everything in data. Then, we have the price in dollars and cents. And also, we have a category that is not changed using the CategoryResource.

So, how do we reuse the CategoryResource inside in the ProductResource? We can call other Resources inside a Resource.

app/Http/Resources/ProductResource.php:

class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'category_id' => $this->category_id,
'name' => $this->name,
'description' => $this->description,
'price' => number_format($this->price / 100, 2),
'category' => $this->category,
'category' => CategoryResource::make($this->whenLoaded('category')),
];
}
}

We also use the conditional relationship whenLoaded to show the category only when it is eager loaded. This way, it is easier to avoid N+1 query problems. Now we can see that category is being shown also using API Resource.

Previous: Why 404 Page? Setting Correct Headers
avatar
Francisco Ferreira Roque Júnior

if some one have problems at the database/factories/ProductFactory.php step. just remember to also "include Category model on that" -> use App\Models\Category;

👍 1
avatar

Sorry about that, we have updated the lesson!

avatar
You can use Markdown
avatar
Francisco Ferreira Roque Júnior

Great class guys, thank you very much.

I didn't know it was possible to use resources inside resources.

avatar
You can use Markdown
avatar

If we are showing the category info with CategoryResource, does it makes sense to display the "'category_id' => $this->category_id," itself? Is it usually good practice to display it too?

avatar
You can use Markdown
avatar
You can use Markdown