REST API Development

File Uploads via API

22 min Lesson 17 of 35

Introduction to File Uploads in APIs

Handling file uploads through REST APIs requires special consideration because files are binary data that must be transmitted differently from typical JSON payloads. This lesson covers multipart form data, validation, storage strategies, and returning accessible file URLs to clients.

Key Concept: File uploads use the multipart/form-data content type instead of application/json, allowing binary file data to be transmitted alongside other form fields.

Understanding Multipart Form Data

When uploading files via API, the request uses multipart/form-data encoding. This format allows multiple data types (text fields and files) to be transmitted in a single request:

POST /api/posts HTTP/1.1 Host: example.com Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Authorization: Bearer your-token-here ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="title" My Blog Post ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="content" This is the post content ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="image"; filename="photo.jpg" Content-Type: image/jpeg [Binary file data here] ------WebKitFormBoundary7MA4YWxkTrZu0gW--

Basic File Upload Controller

Here\047s a simple Laravel controller that handles file uploads:

<?php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; class FileUploadController extends Controller { public function upload(Request $request) { // Validate the uploaded file $request->validate([ \047file\047 => \047required|file|max:10240\047, // Max 10MB ]); // Store the file $path = $request->file(\047file\047)->store(\047uploads\047, \047public\047); // Return the file URL return response()->json([ \047success\047 => true, \047message\047 => \047File uploaded successfully\047, \047data\047 => [ \047path\047 => $path, \047url\047 => Storage::url($path), \047size\047 => $request->file(\047file\047)->getSize(), \047mime_type\047 => $request->file(\047file\047)->getMimeType(), ], ], 201); } }

Comprehensive File Validation

Proper validation is critical for security and user experience:

<?php public function uploadImage(Request $request) { $validator = Validator::make($request->all(), [ \047image\047 => [ \047required\047, \047file\047, \047image\047, // Must be an image \047mimes:jpeg,png,jpg,gif,webp\047, // Allowed formats \047max:5120\047, // Max 5MB \047dimensions:min_width=100,min_height=100,max_width=4000,max_height=4000\047, ], \047title\047 => \047sometimes|string|max:255\047, \047alt_text\047 => \047sometimes|string|max:255\047, ]); if ($validator->fails()) { return response()->json([ \047success\047 => false, \047message\047 => \047Validation failed\047, \047errors\047 => $validator->errors(), ], 422); } // Process the upload $image = $request->file(\047image\047); // Additional security: Check actual file content $imageInfo = getimagesize($image->getPathname()); if ($imageInfo === false) { return response()->json([ \047success\047 => false, \047message\047 => \047Invalid image file\047, ], 422); } // Store with custom filename $filename = time() . \047_\047 . uniqid() . \047.\047 . $image->extension(); $path = $image->storeAs(\047images\047, $filename, \047public\047); return response()->json([ \047success\047 => true, \047data\047 => [ \047url\047 => Storage::url($path), \047path\047 => $path, \047size\047 => $image->getSize(), \047width\047 => $imageInfo[0], \047height\047 => $imageInfo[1], ], ], 201); }

Security Warning: Never trust the client-provided MIME type or file extension alone. Always validate the actual file content to prevent malicious uploads. Use getimagesize() for images and appropriate validation for other file types.

Storage Options and Configuration

Laravel supports multiple storage drivers. Configure them in config/filesystems.php:

<?php return [ \047default\047 => env(\047FILESYSTEM_DISK\047, \047local\047), \047disks\047 => [ // Local storage \047local\047 => [ \047driver\047 => \047local\047, \047root\047 => storage_path(\047app\047), ], // Public storage (web-accessible) \047public\047 => [ \047driver\047 => \047local\047, \047root\047 => storage_path(\047app/public\047), \047url\047 => env(\047APP_URL\047).\047/storage\047, \047visibility\047 => \047public\047, ], // Amazon S3 \047s3\047 => [ \047driver\047 => \047s3\047, \047key\047 => env(\047AWS_ACCESS_KEY_ID\047), \047secret\047 => env(\047AWS_SECRET_ACCESS_KEY\047), \047region\047 => env(\047AWS_DEFAULT_REGION\047), \047bucket\047 => env(\047AWS_BUCKET\047), \047url\047 => env(\047AWS_URL\047), ], ], ];

Using Cloud Storage (Amazon S3)

For production applications, cloud storage is recommended:

<?php // Install AWS SDK: composer require league/flysystem-aws-s3-v3 public function uploadToS3(Request $request) { $request->validate([ \047file\047 => \047required|file|max:10240\047, ]); $file = $request->file(\047file\047); // Generate unique filename $filename = \047uploads/\047 . date(\047Y/m/d\047) . \047/\047 . uniqid() . \047.\047 . $file->extension(); // Upload to S3 with public visibility $path = Storage::disk(\047s3\047)->putFileAs( \047\047, $file, $filename, \047public\047 ); // Get the public URL $url = Storage::disk(\047s3\047)->url($path); return response()->json([ \047success\047 => true, \047data\047 => [ \047url\047 => $url, \047path\047 => $path, \047size\047 => $file->getSize(), ], ], 201); }

Multiple File Uploads

Handling multiple files in a single request:

<?php public function uploadMultiple(Request $request) { $request->validate([ \047files\047 => \047required|array|max:10\047, \047files.*\047 => \047required|file|mimes:jpeg,png,pdf,doc,docx|max:5120\047, ]); $uploadedFiles = []; foreach ($request->file(\047files\047) as $file) { $path = $file->store(\047uploads\047, \047public\047); $uploadedFiles[] = [ \047original_name\047 => $file->getClientOriginalName(), \047path\047 => $path, \047url\047 => Storage::url($path), \047size\047 => $file->getSize(), \047mime_type\047 => $file->getMimeType(), ]; } return response()->json([ \047success\047 => true, \047message\047 => count($uploadedFiles) . \047 files uploaded successfully\047, \047data\047 => $uploadedFiles, ], 201); }

Image Processing and Thumbnails

Create thumbnails or process images using the Intervention Image library:

<?php // Install: composer require intervention/image use Intervention\Image\Facades\Image; public function uploadWithThumbnail(Request $request) { $request->validate([ \047image\047 => \047required|image|max:10240\047, ]); $image = $request->file(\047image\047); $filename = time() . \047_\047 . uniqid() . \047.\047 . $image->extension(); // Store original $originalPath = $image->storeAs(\047images/original\047, $filename, \047public\047); // Create thumbnail $thumbnailImage = Image::make($image) ->fit(300, 300) ->encode($image->extension(), 90); Storage::disk(\047public\047)->put( \047images/thumbnails/\047 . $filename, $thumbnailImage ); // Create medium size $mediumImage = Image::make($image) ->resize(800, null, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }) ->encode($image->extension(), 90); Storage::disk(\047public\047)->put( \047images/medium/\047 . $filename, $mediumImage ); return response()->json([ \047success\047 => true, \047data\047 => [ \047original\047 => Storage::url($originalPath), \047medium\047 => Storage::url(\047images/medium/\047 . $filename), \047thumbnail\047 => Storage::url(\047images/thumbnails/\047 . $filename), ], ], 201); }

Performance Tip: Process images asynchronously using queues for better API response times. Store the original immediately and create thumbnails in the background.

Chunked File Uploads for Large Files

For very large files, implement chunked uploads:

<?php public function uploadChunk(Request $request) { $request->validate([ \047chunk\047 => \047required|file\047, \047chunk_number\047 => \047required|integer|min:0\047, \047total_chunks\047 => \047required|integer|min:1\047, \047file_name\047 => \047required|string\047, \047upload_id\047 => \047required|string\047, ]); $uploadId = $request->upload_id; $chunkNumber = $request->chunk_number; $totalChunks = $request->total_chunks; $fileName = $request->file_name; // Store chunk temporarily $chunkPath = \047chunks/\047 . $uploadId . \047/chunk_\047 . $chunkNumber; Storage::put($chunkPath, file_get_contents($request->file(\047chunk\047))); // Check if all chunks are uploaded $uploadedChunks = count(Storage::files(\047chunks/\047 . $uploadId)); if ($uploadedChunks === $totalChunks) { // Merge all chunks $finalPath = \047uploads/\047 . $fileName; $finalFile = fopen(Storage::path($finalPath), \047wb\047); for ($i = 0; $i < $totalChunks; $i++) { $chunkContent = Storage::get(\047chunks/\047 . $uploadId . \047/chunk_\047 . $i); fwrite($finalFile, $chunkContent); } fclose($finalFile); // Clean up chunks Storage::deleteDirectory(\047chunks/\047 . $uploadId); return response()->json([ \047success\047 => true, \047message\047 => \047File upload completed\047, \047data\047 => [ \047url\047 => Storage::url($finalPath), \047path\047 => $finalPath, ], ], 201); } return response()->json([ \047success\047 => true, \047message\047 => \047Chunk uploaded\047, \047data\047 => [ \047uploaded_chunks\047 => $uploadedChunks, \047total_chunks\047 => $totalChunks, \047progress\047 => round(($uploadedChunks / $totalChunks) * 100, 2), ], ], 200); }

File Download Endpoint

Provide secure file downloads through your API:

<?php public function download($id) { $file = File::findOrFail($id); // Check authorization if (!auth()->user()->can(\047download\047, $file)) { return response()->json([ \047success\047 => false, \047message\047 => \047Unauthorized\047, ], 403); } // Check if file exists if (!Storage::disk(\047private\047)->exists($file->path)) { return response()->json([ \047success\047 => false, \047message\047 => \047File not found\047, ], 404); } // Log download $file->increment(\047download_count\047); // Return file return response()->download( Storage::disk(\047private\047)->path($file->path), $file->original_name ); }

Temporary/Signed URLs

Generate temporary URLs for secure file access:

<?php public function getTemporaryUrl($id) { $file = File::findOrFail($id); // Check authorization if (!auth()->user()->can(\047view\047, $file)) { return response()->json([ \047success\047 => false, \047message\047 => \047Unauthorized\047, ], 403); } // Generate temporary URL (valid for 30 minutes) $url = Storage::disk(\047s3\047)->temporaryUrl( $file->path, now()->addMinutes(30) ); return response()->json([ \047success\047 => true, \047data\047 => [ \047url\047 => $url, \047expires_at\047 => now()->addMinutes(30)->toIso8601String(), ], ]); }

Exercise: Build a File Upload API

Create a complete file upload system:

  1. Create an endpoint that accepts image uploads with validation
  2. Generate three sizes: thumbnail (150x150), medium (800px wide), and original
  3. Store files on Amazon S3 or local storage
  4. Return URLs for all three sizes
  5. Create an endpoint to list all uploaded files with pagination
  6. Implement file deletion with authorization checks
  7. Add a temporary URL generator for secure file access

Best Practices for File Uploads

  • Validate Thoroughly: Check file type, size, and content, not just extension
  • Limit File Sizes: Set reasonable limits based on your application needs
  • Use Unique Names: Generate unique filenames to prevent collisions and overwriting
  • Sanitize Filenames: Remove or replace dangerous characters in user-provided filenames
  • Store Privately: Keep sensitive files outside the public directory
  • Scan for Malware: Use antivirus scanning for user-uploaded files in production
  • Set Upload Limits: Configure max upload sizes in both PHP and your web server
  • Use CDN: Serve static files through a CDN for better performance
  • Implement Quotas: Limit total storage per user to prevent abuse
  • Clean Up: Regularly delete orphaned or temporary files

Production Consideration: For high-traffic applications, consider using a dedicated file processing service or queue system to handle uploads asynchronously. This prevents blocking your API and provides better scalability.