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
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
<?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',
];
}
}
UpdateMenuCategoryRequest
<?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',
];
}
}
Using sometimes allows partial updates with PATCH.
4. API Resource
The Resource controls the data returned to the client.
<?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
<?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,
]);
}
}
6. API Routes
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:
Bashphp artisan route:list
7. Route Model Binding
Instead of manually finding a record:
PHP
public function show(MenuCategory $menuCategory)
Laravel automatically resolves the model from the URL.
For example:
GET /api/v1/menu-categories/10
This is similar to Django's:
get_object_or_404(MenuCategory, id=10)
get_object_or_404(MenuCategory, id=10)
8. API Response Standard
A successful response can follow this structure:
{
"success": true,
"message": "Menu category created successfully.",
"data": {}
}
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:
{
"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
$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
POST /api/v1/menu-categories
{
"name": "Burgers",
"warehouse_id": 1,
"sort_order": 1,
"kitchen_section_id": 2,
"is_active": true
}
Update:
PATCH /api/v1/menu-categories/1
{
"is_active": false
}
http
Retrieve:
httpGET /api/v1/menu-categories/1
http
List:
httpGET /api/v1/menu-categories
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
Top comments (0)