DEV Community

Vincent Tommi
Vincent Tommi

Posted on

Building a Laravel CRUD API: Menu Categories for Django Developers

As a Django developer learning Laravel, I wanted to understand how Laravel handles CRUD APIs.

In Django REST Framework, I would normally use a model, serializer, ViewSet, and router. Laravel uses similar concepts, but the structure is different.

In this article, I'll build a menu-categories API with:

  • Create
  • List
  • Retrieve
  • Update
  • Delete
  • Validation
  • Pagination
  • Route model binding
  • Standard API responses
  • Protected deletion

Django vs Laravel

Django REST Framework Laravel
Model Eloquent Model
Serializer Form Request + API Resource
ViewSet Controller
urls.py routes/api.php
serializer.is_valid() Form Request
serializer.data API Resource
DRF Router Route::apiResource()
get_object_or_404() Route Model Binding

1. Create the Laravel Files

Laravel Artisan makes this simple:

php artisan make:controller Api/MenuCategoryController
php artisan make:request StoreMenuCategoryRequest
php artisan make:request UpdateMenuCategoryRequest
php artisan make:resource MenuCategoryResource

The structure is:
text

Enter fullscreen mode Exit fullscreen mode

app/
├── Http/
│ ├── Controllers/Api/MenuCategoryController.php
│ ├── Requests/
│ │ ├── StoreMenuCategoryRequest.php
│ │ └── UpdateMenuCategoryRequest.php
│ └── Resources/MenuCategoryResource.php
└── Models/MenuCategory.php

2. Model
Our MenuCategory model contains relationships with warehouses, kitchen sections, and menu items:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class MenuCategory extends Model
{
    protected $fillable = [
        'name',
        'warehouse_id',
        'sort_order',
        'kitchen_section_id',
        'is_active',
    ];

    protected $casts = [
        'is_active' => 'boolean',
        'sort_order' => 'integer',
    ];

    public function warehouse(): BelongsTo
    {
        return $this->belongsTo(Warehouse::class);
    }

    public function kitchenSection(): BelongsTo
    {
        return $this->belongsTo(KitchenSection::class);
    }

    public function menuItems(): HasMany
    {
        return $this->hasMany(MenuItem::class);
    }
}
A NULL warehouse_id means the category can be available to all warehouses.

3. Request Validation
Instead of putting validation inside the controller, Laravel uses Form Requests.
StoreMenuCategoryRequest

Enter fullscreen mode Exit fullscreen mode

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreMenuCategoryRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

public function rules(): array
{
    return [
        'name' => 'required|string|max:255',
        'warehouse_id' => 'nullable|integer|exists:warehouses,id',
        'sort_order' => 'nullable|integer|min:0',
        'kitchen_section_id' => 'nullable|integer|exists:kitchen_sections,id',
        'is_active' => 'sometimes|boolean',
    ];
}
Enter fullscreen mode Exit fullscreen mode

}

UpdateMenuCategoryRequest

Enter fullscreen mode Exit fullscreen mode

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateMenuCategoryRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

public function rules(): array
{
    return [
        'name' => 'sometimes|string|max:255',
        'warehouse_id' => 'sometimes|nullable|integer|exists:warehouses,id',
        'sort_order' => 'sometimes|integer|min:0',
        'kitchen_section_id' => 'sometimes|nullable|integer|exists:kitchen_sections,id',
        'is_active' => 'sometimes|boolean',
    ];
}
Enter fullscreen mode Exit fullscreen mode

}

Using sometimes allows partial updates with PATCH.

4. API Resource
The Resource controls the data returned to the client.

Enter fullscreen mode Exit fullscreen mode

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class MenuCategoryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'warehouse_id' => $this->warehouse_id,
'sort_order' => $this->sort_order,
'kitchen_section_id' => $this->kitchen_section_id,
'is_active' => $this->is_active,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

This is similar to a serializer in Django REST Framework.
5. Controller
The controller handles the CRUD operations:
PHP

Enter fullscreen mode Exit fullscreen mode

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreMenuCategoryRequest;
use App\Http\Requests\UpdateMenuCategoryRequest;
use App\Http\Resources\MenuCategoryResource;
use App\Models\MenuCategory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class MenuCategoryController extends Controller
{
public function index(): AnonymousResourceCollection
{
$categories = MenuCategory::query()
->orderBy('sort_order')
->orderBy('name')
->paginate(20);

    return MenuCategoryResource::collection($categories);
}

public function store(StoreMenuCategoryRequest $request): JsonResponse
{
    $category = MenuCategory::create(
        $request->validated()
    );

    return response()->json([
        'success' => true,
        'message' => 'Menu category created successfully.',
        'data' => new MenuCategoryResource($category),
    ], 201);
}

public function show(MenuCategory $menuCategory): JsonResponse
{
    return response()->json([
        'success' => true,
        'message' => 'Menu category retrieved successfully.',
        'data' => new MenuCategoryResource($menuCategory),
    ]);
}

public function update(
    UpdateMenuCategoryRequest $request,
    MenuCategory $menuCategory
): JsonResponse {
    $menuCategory->update(
        $request->validated()
    );

    return response()->json([
        'success' => true,
        'message' => 'Menu category updated successfully.',
        'data' => new MenuCategoryResource($menuCategory),
    ]);
}

public function destroy(MenuCategory $menuCategory): JsonResponse
{
    if ($menuCategory->menuItems()->exists()) {
        return response()->json([
            'success' => false,
            'message' => 'Menu category cannot be deleted because it has menu items.',
            'data' => null,
        ], 409);
    }

    $menuCategory->delete();

    return response()->json([
        'success' => true,
        'message' => 'Menu category deleted successfully.',
        'data' => null,
    ]);
}
Enter fullscreen mode Exit fullscreen mode

}

6. API Routes

Enter fullscreen mode Exit fullscreen mode

In routes/api.php:
PHP<?php

use App\Http\Controllers\Api\MenuCategoryController;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')->group(function () {
Route::apiResource(
'menu-categories',
MenuCategoryController::class
);
});

Laravel generates:

GET    /api/v1/menu-categories
POST   /api/v1/menu-categories
GET    /api/v1/menu-categories/{id}
PUT/PATCH /api/v1/menu-categories/{id}
DELETE /api/v1/menu-categories/{id}


Check them with:

Enter fullscreen mode Exit fullscreen mode

Bashphp artisan route:list

7. Route Model Binding
Instead of manually finding a record:
PHP
Enter fullscreen mode Exit fullscreen mode

public function show(MenuCategory $menuCategory)

Laravel automatically resolves the model from the URL.
For example:

Enter fullscreen mode Exit fullscreen mode

GET /api/v1/menu-categories/10


This is similar to Django's:

Enter fullscreen mode Exit fullscreen mode

get_object_or_404(MenuCategory, id=10)


Enter fullscreen mode Exit fullscreen mode

get_object_or_404(MenuCategory, id=10)

8. API Response Standard
A successful response can follow this structure:

Enter fullscreen mode Exit fullscreen mode

{
"success": true,
"message": "Menu category created successfully.",
"data": {}
}


Enter fullscreen mode Exit fullscreen mode

An error:

{
"success": false,
"message": "Menu category cannot be deleted because it has menu items.",
"data": null
}

For validation errors, I would standardize the application's exception handling so all endpoints return:

Enter fullscreen mode Exit fullscreen mode

{
"success": false,
"message": "Validation failed.",
"data": null,
"errors": {}
}


9. Database Constraints
If category names should be unique within a warehouse, enforce this at the database level:
PHP

Enter fullscreen mode Exit fullscreen mode

$table->unique(
['warehouse_id', 'name'],
'menu_categories_warehouse_name_unique'
);


This allows the same category name in different warehouses but prevents duplicates within the same warehouse.
Database constraints are important because application-level validation alone cannot fully protect against concurrent requests

10. Testing the API
Create:
http

Enter fullscreen mode Exit fullscreen mode

POST /api/v1/menu-categories


Enter fullscreen mode Exit fullscreen mode

{
"name": "Burgers",
"warehouse_id": 1,
"sort_order": 1,
"kitchen_section_id": 2,
"is_active": true
}

Update:

Enter fullscreen mode Exit fullscreen mode

PATCH /api/v1/menu-categories/1

Enter fullscreen mode Exit fullscreen mode

{
"is_active": false
}



Enter fullscreen mode Exit fullscreen mode


http
Retrieve:
httpGET /api/v1/menu-categories/1


Enter fullscreen mode Exit fullscreen mode


http
List:
httpGET /api/v1/menu-categories


Enter fullscreen mode Exit fullscreen mode


http
Delete:
httpDELETE /api/v1/menu-categories/1



Laravel API Flow


HTTP Request
    ↓
Route
    ↓
Controller
    ↓
Form Request
    ↓
Validation
    ↓
Eloquent Model
    ↓
Database
    ↓
API Resource
    ↓
JSON Response


The Django REST Framework equivalent is roughly:

HTTP Request
    ↓
URL / Router
    ↓
ViewSet
    ↓
Serializer
    ↓
Validation
    ↓
Django Model
    ↓
Database
    ↓
Serializer Response

Final Thoughts
Coming from Django, Laravel initially felt unfamiliar because the terminology and structure are different.
Once I mapped the concepts, the architecture became much easier to understand:

Django Model → Laravel Eloquent Model
DRF Serializer → Form Request + API Resource
DRF ViewSet → Laravel Controller
DRF Router → Laravel apiResource
get_object_or_404() → Route Model Binding

The main lesson is that a production CRUD API is more than implementing POST, GET, PUT, and DELETE.
It should also consider:


Validation
Correct HTTP status codes
Consistent responses
Pagination
Database constraints
Referential integrity
Protected deletion
Separation of responsibilities

This small menu-categories API gave me a practical way to understand how Laravel compares with Django REST Framework.
text
Enter fullscreen mode Exit fullscreen mode

Top comments (0)