When working with Laravel and Inertia.js, especially when handling file uploads in update operations, you might encounter a common pattern that seems counterintuitive at first: using POST requests with _method: "PUT" instead of actual PUT requests. This article explains why this approach is necessary and how to implement it correctly.
The Problem: HTTP Methods and File Uploads
Understanding HTTP Methods
In RESTful APIs, different HTTP methods serve specific purposes:
GET- Retrieve dataPOST- Create new resourcesPUT- Update existing resources (complete replacement)PATCH- Partial updatesDELETE- Remove resources
Logically, when updating a resource, we should use PUT or PATCH. However, there’s a technical limitation that forces us to use a different approach.
The File Upload Limitation
The core issue lies in how browsers handle file uploads with different HTTP methods:
- POST requests can handle
multipart/form-dataencoding, which is required for file uploads - PUT requests in browsers don’t properly support
multipart/form-data - Most browsers only support
GETandPOSTmethods natively in HTML forms
The Solution: Method Spoofing
Laravel provides an elegant solution called “method spoofing” using the _method field.
How Method Spoofing Works
// Instead of this (which won't work for files):
router.put('/posts/1', {
title: 'Updated Title',
content: 'Updated content',
image: fileObject // This won't work properly
});
// We use this:
router.post('/posts/1', {
_method: 'PUT',
title: 'Updated Title',
content: 'Updated content',
image: fileObject // This works!
});
Laravel’s middleware intercepts the _method field and treats the request as if it were sent with the specified HTTP method.
Implementation Examples
Create Operation (Standard POST)
Frontend (React with Inertia.js):
import { useForm } from '@inertiajs/react';
function CreatePost() {
const { data, setData, post, processing, errors } = useForm({
title: '',
content: '',
image: null,
});
const handleSubmit = (e) => {
e.preventDefault();
post('/posts', {
forceFormData: true, // Ensures multipart/form-data
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={data.title}
onChange={e => setData('title', e.target.value)}
placeholder="Title"
/>
<textarea
value={data.content}
onChange={e => setData('content', e.target.value)}
placeholder="Content"
/>
<input
type="file"
onChange={e => setData('image', e.target.files[0])}
/>
<button type="submit" disabled={processing}>
Create Post
</button>
</form>
);
}
Backend (Laravel Controller):
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'image' => 'nullable|image|max:2048',
]);
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')->store('posts', 'public');
}
Post::create($validated);
return redirect()->route('posts.index')
->with('success', 'Post created successfully!');
}
}
Update Operation (POST with _method: PUT)
Frontend (React with Inertia.js):
import { useForm } from '@inertiajs/react';
function EditPost({ post }) {
const { data, setData, post: submit, processing, errors } = useForm({
title: post.title,
content: post.content,
image: null,
_method: 'PUT', // This is crucial!
});
const handleSubmit = (e) => {
e.preventDefault();
// Note: We use POST here, not PUT
submit(`/posts/${post.id}`, {
forceFormData: true,
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={data.title}
onChange={e => setData('title', e.target.value)}
placeholder="Title"
/>
<textarea
value={data.content}
onChange={e => setData('content', e.target.value)}
placeholder="Content"
/>
<input
type="file"
onChange={e => setData('image', e.target.files[0])}
/>
{post.image && (
<div>
<p>Current image:</p>
<img src={`/storage/${post.image}`} alt="Current" width="200" />
</div>
)}
<button type="submit" disabled={processing}>
Update Post
</button>
</form>
);
}
Backend (Laravel Controller):
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'image' => 'nullable|image|max:2048',
]);
// Handle file upload
if ($request->hasFile('image')) {
// Delete old image if exists
if ($post->image) {
Storage::disk('public')->delete($post->image);
}
$validated['image'] = $request->file('image')->store('posts', 'public');
}
$post->update($validated);
return redirect()->route('posts.index')
->with('success', 'Post updated successfully!');
}
Routes (web.php):
Route::resource('posts', PostController::class);
Key Differences Between Create and Update
| Aspect | Create (POST) | Update (POST + _method: PUT) |
|---|---|---|
| HTTP Method | POST | POST (with _method: 'PUT') |
| Route | /posts | /posts/{id} |
| Form Data | Standard fields | Standard fields + _method |
| Laravel Method | store() | update() |
| Purpose | Create new resource | Update existing resource |
Important Configuration Notes
1. Inertia.js Configuration
Make sure to use forceFormData: true when dealing with files:
submit(`/posts/${post.id}`, {
forceFormData: true, // This ensures proper multipart encoding
});
2. File Validation
Always validate file uploads properly:
$request->validate([
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
Common Pitfalls and Solutions
Pitfall 1: Using Actual PUT Request for Files
// ❌ This won't work properly with files
router.put('/posts/1', formData);
// ✅ Use this instead
router.post('/posts/1', {
...formData,
_method: 'PUT'
});
Pitfall 2: Forgetting forceFormData
// ❌ Files might not upload correctly
submit(`/posts/${post.id}`);
// ✅ Always use forceFormData with files
submit(`/posts/${post.id}`, {
forceFormData: true
});
Pitfall 3: Not Handling Existing Files
// ❌ This will lose the existing image if no new one is uploaded
public function update(Request $request, Post $post)
{
$post->update($request->validated());
}
// ✅ Handle file updates properly
public function update(Request $request, Post $post)
{
$validated = $request->validated();
if ($request->hasFile('image')) {
// Delete old image
if ($post->image) {
Storage::disk('public')->delete($post->image);
}
$validated['image'] = $request->file('image')->store('posts', 'public');
}
$post->update($validated);
}
Conclusion
The use of POST with _method: "PUT" in Laravel file uploads isn’t just a convention - it’s a necessary workaround for browser and HTTP limitations. This approach:
- Maintains RESTful principles by using method spoofing
- Ensures proper file handling through multipart/form-data encoding
- Provides consistent routing with Laravel’s resource controllers
- Works seamlessly with Inertia.js and modern frontend frameworks
Understanding this pattern is crucial for any Laravel developer working with file uploads. While it might seem counterintuitive at first, it’s the standard way to handle file updates in modern web applications.
Remember: when in doubt with file uploads in Laravel updates, always use POST with _method field - your future self will thank you!