Last updated: July 2026 – verified on Laravel 11/12
File uploads in Laravel take three pieces: a form with the right encoding, validation, and the store() call. Here is the whole flow with the current best practices.
1. The form
<form action="{{ route('documents.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="file" name="document">
@error('document') <p class="error">{{ $message }}</p> @enderror
<button type="submit">Upload</button>
</form>
enctype="multipart/form-data" is the piece everyone forgets — without it $request->file() returns null.
2. Validate, then store
public function store(Request $request)
{
$validated = $request->validate([
'document' => ['required', 'file', 'mimes:pdf,docx,jpg,png', 'max:5120'], // max in KB
]);
$path = $request->file('document')->store('documents', 'public');
// $path => "documents/kJh2...x8.pdf" (hashed filename, collision-safe)
return back()->with('status', 'Uploaded to ' . $path);
}
Key points: max:5120 is kilobytes (5 MB); store() generates a hashed filename automatically so users can’t overwrite each other’s files; the second argument picks the disk from config/filesystems.php.
To keep the original name, use storeAs() — but sanitize it:
$name = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$path = $file->storeAs('documents', str($name)->slug() . '.' . $file->extension(), 'public');
3. Make public files reachable
php artisan storage:link
This symlinks public/storage to storage/app/public, so the file is served at:
<a href="{{ Storage::url($path) }}">Download</a>
4. Multiple files
<input type="file" name="attachments[]" multiple>
$request->validate(['attachments.*' => ['file', 'mimes:jpg,png', 'max:2048']]);
foreach ($request->file('attachments', []) as $file) {
$file->store('attachments', 'public');
}
5. Errors you will hit
- File is null / validation says required → missing
enctype, or the file exceeds PHP’s own limits. Raiseupload_max_filesizeandpost_max_sizein php.ini above your Laravelmax:rule. - 413 Request Entity Too Large → the web server, not Laravel: raise
client_max_body_sizein nginx. - File visible in storage/ but 404 in browser → you forgot
php artisan storage:link, or the symlink didn’t survive deployment (re-run it in your deploy script).
For upload UIs with live progress and previews, Livewire handles the same flow reactively — see my Livewire dynamic form builder with file uploads.can then access the uploaded file at the generated URL.
