GICT knowledge base
Handling PATCH and PUT Requests with Next.js and Laravel
Published
In today’s fast-paced digital world, users expect seamless and instant updates to their data without page refreshes or delays. Whether you’re building a user profile system, content management platform, or any data-driven application, mastering HTTP PATCH and PUT requests is essential for creating modern, responsive web applications.
This comprehensive guide will walk you through implementing efficient data updates using Next.js 14 (App Router) as the frontend and Laravel 10 as the backend API. You’ll learn how to create real-time partial updates, handle full resource replacements, implement proper error handling, and build a production-ready system.
What You’ll Learn
By the end of this tutorial, you’ll be able to:
- Set up Laravel API endpoints for PATCH and PUT requests
- Create Next.js API routes that communicate with Laravel
- Build interactive React components with real-time updates
- Implement proper validation and error handling
- Understand when to use PATCH vs PUT requests
- Test your implementation effectively
Prerequisites
Before we dive in, make sure you have:
- PHP 8.1+ with Composer installed
- Node.js 18+ with npm/yarn
- Basic knowledge of Laravel and React/Next.js
- A code editor (VS Code recommended)
Project Setup Commands
Let’s start by setting up both projects from scratch.
Laravel Backend Setup
# Create new Laravel project
composer create-project laravel/laravel laravel-api
cd laravel-api
# Install dependencies
composer install
# Set up environment
cp .env.example .env
php artisan key:generate
# Create database and run migrations
php artisan migrate
# Create User model and controller
php artisan make:controller UserController --resource
php artisan make:model User -m
# Install Laravel Sanctum for API authentication (optional)
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
# Start Laravel development server
php artisan serve
Next.js Frontend Setup
# Create new Next.js project
npx create-next-app@latest nextjs-frontend --typescript --tailwind --eslint --app
cd nextjs-frontend
# Install additional dependencies
npm install axios swr
# Start Next.js development server
npm run dev
Understanding PATCH vs PUT
Before diving into implementation, let’s understand the key differences:
- PUT: Replaces the entire resource with new data (all fields must be provided)
- PATCH: Updates only specific fields of a resource (partial updates)
Laravel Backend Implementation
Step 1: Database Migration
First, let’s create a proper users table migration:
# Create migration for users table (if not exists)
php artisan make:migration create_users_table
# Or modify existing migration
php artisan make:migration add_phone_to_users_table --table=users
Update your migration file:
<?php
// database/migrations/xxxx_xx_xx_create_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('phone')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('users');
}
};
Run the migration:
php artisan migrate
Step 2: Create API Routes
First, add routes to your routes/api.php file:
<?php
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
Route::middleware('api')->group(function () {
Route::put('/users/{id}', [UserController::class, 'update']);
Route::patch('/users/{id}', [UserController::class, 'partialUpdate']);
});
Step 3: Create Controller Methods
Generate the controller and add methods:
# Create controller if not exists
php artisan make:controller Api/UserController --resource
Create a controller to handle both request types:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
// Handle PUT requests (full update)
public function update(Request $request, $id)
{
$user = User::findOrFail($id);
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $id,
'phone' => 'required|string|max:20'
]);
$user->update($validatedData);
return response()->json([
'success' => true,
'message' => 'User updated successfully',
'user' => $user
]);
}
// Handle PATCH requests (partial update)
public function partialUpdate(Request $request, $id)
{
$user = User::findOrFail($id);
$validatedData = $request->validate([
'name' => 'sometimes|string|max:255',
'email' => 'sometimes|email|unique:users,email,' . $id,
'phone' => 'sometimes|string|max:20'
]);
$user->update($validatedData);
return response()->json([
'success' => true,
'message' => 'User partially updated',
'user' => $user
]);
}
}
Step 4: Configure CORS
Install and configure Laravel CORS:
# Install CORS package (if not already installed)
composer require fruitcake/laravel-cors
# Publish CORS config
php artisan vendor:publish --tag="cors"
Update your Laravel application to allow PATCH and PUT methods in config/cors.php:
<?php
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:3000'],
'allowed_headers' => ['*'],
];
Next.js Frontend Implementation
Step 1: Project Structure Setup
First, let’s organize our Next.js project:
# Create necessary directories
mkdir -p components hooks lib
mkdir -p app/api/users/[id]
# Create environment file
touch .env.local
Add to your .env.local:
LARAVEL_API_URL=http://localhost:8000/api
Step 2: Create API Route Handlers
Create app/api/users/[id]/route.js:
const LARAVEL_API_URL = process.env.LARAVEL_API_URL || 'http://localhost:8000/api';
export async function PUT(request, { params }) {
const { id } = params;
const data = await request.json();
try {
const response = await fetch(`${LARAVEL_API_URL}/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (!response.ok) {
return Response.json(result, { status: response.status });
}
return Response.json(result);
} catch (error) {
return Response.json(
{ success: false, message: 'Update failed' },
{ status: 500 }
);
}
}
export async function PATCH(request, { params }) {
const { id } = params;
const data = await request.json();
try {
const response = await fetch(`${LARAVEL_API_URL}/users/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (!response.ok) {
return Response.json(result, { status: response.status });
}
return Response.json(result);
} catch (error) {
return Response.json(
{ success: false, message: 'Partial update failed' },
{ status: 500 }
);
}
}
Step 3: Create User Form Component
Create the components directory and form:
# Create component file
touch components/UserForm.js
Create components/UserForm.js:
'use client';
import { useState } from 'react';
export default function UserForm({ userId, initialData }) {
const [formData, setFormData] = useState(initialData || {
name: '',
email: '',
phone: ''
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [errors, setErrors] = useState({});
// Handle full update with PUT
const handleFullUpdate = async (e) => {
e.preventDefault();
setLoading(true);
setMessage('');
setErrors({});
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
setMessage('User updated successfully!');
setFormData(result.user);
} else {
setErrors(result.errors || {});
setMessage(result.message || 'Update failed');
}
} catch (error) {
setMessage('Network error occurred');
} finally {
setLoading(false);
}
};
// Handle partial update with PATCH
const handlePartialUpdate = async (field, value) => {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ [field]: value })
});
const result = await response.json();
if (result.success) {
console.log(`${field} updated successfully`);
}
} catch (error) {
console.error('Partial update failed:', error);
}
};
const handleInputChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<div className="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6">Update User</h2>
{message && (
<div className={`p-3 mb-4 rounded ${
message.includes('successfully')
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}>
{message}
</div>
)}
<form onSubmit={handleFullUpdate} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
onBlur={(e) => handlePartialUpdate('name', e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter name"
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name[0]}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter email"
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email[0]}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">Phone</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
onBlur={(e) => handlePartialUpdate('phone', e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter phone number"
/>
{errors.phone && (
<p className="text-red-500 text-sm mt-1">{errors.phone[0]}</p>
)}
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Updating...' : 'Update User (PUT)'}
</button>
</form>
<div className="mt-4 text-sm text-gray-600">
<p><strong>Tip:</strong> Fields update automatically on blur (PATCH), or click button for full update (PUT)</p>
</div>
</div>
);
}
Step 4: Create Page Component
Create the user edit page:
# Create user pages directory
mkdir -p app/users/[id]
touch app/users/[id]/page.js
Create app/users/[id]/page.js:
import UserForm from '@/components/UserForm';
// Mock function to fetch user data
async function getUserData(id) {
// In real app, fetch from your API
return {
name: 'John Doe',
email: '[email protected]',
phone: '+1234567890'
};
}
export default async function UserEditPage({ params }) {
const userData = await getUserData(params.id);
return (
<div className="min-h-screen bg-gray-100 py-8">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold text-center mb-8">Edit User Profile</h1>
<UserForm userId={params.id} initialData={userData} />
</div>
</div>
);
}
Custom Hook for API Calls
Create a reusable hook for cleaner code:
# Create hooks directory and file
mkdir -p hooks
touch hooks/useUserUpdate.js
Create a reusable hook hooks/useUserUpdate.js:
import { useState } from 'react';
export function useUserUpdate() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const updateUser = async (userId, data, method = 'PUT') => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`, {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'Update failed');
}
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
};
const fullUpdate = (userId, data) => updateUser(userId, data, 'PUT');
const partialUpdate = (userId, data) => updateUser(userId, data, 'PATCH');
return { fullUpdate, partialUpdate, loading, error };
}
Error Handling Best Practices
Laravel Error Responses
public function update(Request $request, $id)
{
try {
$user = User::findOrFail($id);
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $id,
'phone' => 'required|string|max:20'
]);
$user->update($validatedData);
return response()->json([
'success' => true,
'message' => 'User updated successfully',
'user' => $user
]);
} catch (\Illuminate\Validation\ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'Validation failed',
'errors' => $e->errors()
], 422);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return response()->json([
'success' => false,
'message' => 'User not found'
], 404);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Server error occurred'
], 500);
}
}
Environment Configuration
Create .env variables for your Next.js app:
LARAVEL_API_URL=http://localhost:8000/api
Testing Your Implementation
1. Laravel Artisan Commands
# Create seeder for test data
php artisan make:seeder UserSeeder
# Create factory for generating test users
php artisan make:factory UserFactory
# Run seeders
php artisan db:seed --class=UserSeeder
# Clear cache if needed
php artisan cache:clear
php artisan config:clear
php artisan route:clear
2. API Testing with cURL
Test your Laravel API directly:
# Test PUT Request (Full Update)
curl -X PUT http://localhost:8000/api/users/1 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "John Updated",
"email": "[email protected]",
"phone": "+1987654321"
}'
# Test PATCH Request (Partial Update)
curl -X PATCH http://localhost:8000/api/users/1 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "John Partially Updated"
}'
3. Next.js API Testing
Test your Next.js API routes:
# Test Next.js PUT endpoint
curl -X PUT http://localhost:3000/api/users/1 \
-H "Content-Type: application/json" \
-d '{
"name": "John Updated via Next.js",
"email": "[email protected]",
"phone": "+1987654321"
}'
# Test Next.js PATCH endpoint
curl -X PATCH http://localhost:3000/api/users/1 \
-H "Content-Type: application/json" \
-d '{
"name": "John Partially Updated via Next.js"
}'
4. Browser Development Tools
Open your browser’s Network tab to monitor:
- Request methods (PUT/PATCH)
- Response status codes
- Request/Response payloads
- Response times
Additional Useful Commands
Laravel Commands
# View all routes
php artisan route:list --name=users
# Create middleware for logging
php artisan make:middleware LogApiRequests
# Create request validation classes
php artisan make:request UpdateUserRequest
php artisan make:request PartialUpdateUserRequest
# Run tests
php artisan test
# Check Laravel version
php artisan --version
Next.js Commands
# Build for production
npm run build
# Start production server
npm start
# Check Next.js info
npx next info
# Install additional packages for testing
npm install --save-dev jest @testing-library/react
# Run linting
npm run lint
Development Server Commands
Run both servers simultaneously:
# Terminal 1 - Laravel API
cd laravel-api
php artisan serve --port=8000
# Terminal 2 - Next.js Frontend
cd nextjs-frontend
npm run dev
Or use a process manager like PM2:
# Install PM2 globally
npm install -g pm2
# Create ecosystem file
touch ecosystem.config.js
Add to ecosystem.config.js:
module.exports = {
apps: [
{
name: 'laravel-api',
script: 'php',
args: 'artisan serve --port=8000',
cwd: './laravel-api'
},
{
name: 'nextjs-frontend',
script: 'npm',
args: 'run dev',
cwd: './nextjs-frontend'
}
]
};
# Start both applications
pm2 start ecosystem.config.js
# Monitor applications
pm2 monit
Key Benefits
- Efficient Updates: PATCH reduces bandwidth by sending only changed fields
- User Experience: Real-time updates on field blur provide instant feedback
- Error Handling: Comprehensive validation on both frontend and backend
- Scalability: Clean separation between full and partial updates
Common Issues and Solutions
Issue 1: CORS Errors
Solution: Configure Laravel CORS properly for your Next.js domain
Issue 2: 405 Method Not Allowed
Solution: Ensure routes are defined correctly in both Laravel and Next.js
Issue 3: Validation Errors
Solution: Implement consistent validation rules on both frontend and backend
Conclusion
This implementation provides a robust foundation for handling PATCH and PUT requests between Next.js and Laravel. The combination of real-time partial updates (PATCH) and comprehensive full updates (PUT) creates an excellent user experience while maintaining data integrity.
Remember to always validate data on both the client and server sides, implement proper error handling, and consider security implications when building production applications.
