Building a Nested Category API in Laravel 11: A Practical Guide
Nested categories are common in e-commerce platforms, CMS applications, documentation systems, and other products that need hierarchical navigation. In Laravel, a self-referencing Eloquent relationship can model a category that belongs to a parent and has any number of children.
This Laravel 11 example builds a small API for creating and reading category trees.
1. Create the Laravel Project
composer create-project --prefer-dist laravel/laravel laravel-nested-categories
cd laravel-nested-categories
php artisan serve2. Create the Category Model and Migration
php artisan make:model Category -mDefine the table in the generated migration:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->foreignId('parent_id')
->nullable()
->constrained('categories')
->cascadeOnDelete();
$table->timestamps();
});Run the migration:
php artisan migratecascadeOnDelete() means deleting a category also deletes its descendants through the database foreign key. Decide whether that behavior is appropriate for your application before using it in production.
3. Define Parent and Child Relationships
In app/Models/Category.php:
class Category extends Model
{
use HasFactory;
protected $fillable = ['name', 'parent_id'];
public function parent()
{
return $this->belongsTo(Category::class, 'parent_id');
}
public function children()
{
return $this->hasMany(Category::class, 'parent_id');
}
public function childrenRecursive()
{
return $this->children()->with('childrenRecursive');
}
}The regular children() relationship loads one level. childrenRecursive() recursively eager-loads descendants, which is useful when the API needs to return a complete tree.
4. Create the Controller
Generate a controller:
php artisan make:controller CategoryControllerThen add endpoints for reading and creating trees:
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CategoryController extends Controller
{
public function index()
{
$categories = Category::query()
->whereNull('parent_id')
->with('childrenRecursive')
->get();
return response()->json($categories);
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'children' => ['sometimes', 'array'],
'children.*.name' => ['required', 'string', 'max:255'],
'children.*.children' => ['sometimes', 'array'],
]);
$category = DB::transaction(function () use ($data) {
return $this->createCategory($data);
});
return response()->json([
'category' => $category->load('childrenRecursive'),
], 201);
}
private function createCategory(array $data, ?int $parentId = null): Category
{
$category = Category::create([
'name' => $data['name'],
'parent_id' => $parentId,
]);
foreach ($data['children'] ?? [] as $child) {
$this->createCategory($child, $category->id);
}
return $category;
}
}The transaction ensures that a failure while creating one descendant rolls back the entire tree instead of leaving a partially created hierarchy.
For arbitrarily deep input, consider recursive validation or a dedicated request object rather than validating only a few explicitly named levels.
5. Define API Routes
In Laravel 11 projects where API routes have been installed/enabled, add these routes to routes/api.php:
use App\Http\Controllers\CategoryController;
use Illuminate\Support\Facades\Route;
Route::get('/categories', [CategoryController::class, 'index']);
Route::post('/categories', [CategoryController::class, 'store']);6. Test the API
Create a nested category tree:
curl -X POST http://localhost:8000/api/categories \
-H "Content-Type: application/json" \
-d '{
"name": "Electronics",
"children": [
{
"name": "Mobile Phones",
"children": [
{"name": "Smartphones"},
{"name": "Feature Phones"}
]
},
{"name": "Laptops"}
]
}'Fetch root categories and their descendants:
curl http://localhost:8000/api/categoriesConclusion
A self-referencing Eloquent relationship is a straightforward way to model hierarchical categories. Recursive eager loading and recursive creation work well for modest trees, while very deep or extremely large hierarchies may need stricter depth limits, different query strategies, or a more specialized tree representation.