When building modern full-stack applications, properly handling data updates through PATCH and PUT requests is crucial for creating robust, maintainable APIs. Many developers struggle with implementing these HTTP methods correctly, often treating them interchangeably or misunderstanding their proper use cases, which can lead to inconsistent data handling and poor user experiences.
This comprehensive guide will walk you through implementing PATCH and PUT requests between Next.js and Laravel, covering everything from frontend form handling to backend validation and security considerations. By the end, you’ll understand when to use each method, how to implement them properly, and best practices for creating a seamless update experience that follows RESTful standards.
Whether you’re building user profile updates, inventory management systems, or any application requiring data modifications, mastering these HTTP methods will elevate your full-stack development skills and create more predictable, maintainable applications.
Understanding the Role of PATCH and PUT in REST APIs
REST (Representational State Transfer) architecture defines specific HTTP methods for different operations, with PUT and PATCH serving distinct purposes in data modification workflows. Understanding these differences is fundamental to building intuitive APIs that behave predictably for both developers and end users.
PUT requests represent complete resource replacement, meaning the entire resource should be updated with the provided data. When a client sends a PUT request, it’s essentially saying “replace this resource entirely with what I’m sending you.”
PATCH requests represent partial resource modification, allowing clients to send only the fields that need updating. This approach is more efficient for bandwidth and processing, especially when dealing with large objects where only small changes occur.
Why Proper HTTP Method Handling Matters in Full-Stack Development
Correct HTTP method implementation provides several critical benefits:
Semantic Clarity: Other developers (and your future self) can immediately understand the intent of each API call Performance Optimization: PATCH requests reduce payload size and processing overhead Cache Management: Proper HTTP semantics help browsers and CDNs handle caching more effectively API Consistency: Following standards makes your API more predictable and easier to integrate Error Handling: Different methods allow for more specific error responses and recovery strategies
Fundamentals of PATCH and PUT Methods
What is a PUT Request and When to Use It
PUT requests follow the principle of idempotency, meaning multiple identical requests should have the same effect as a single request. When implementing PUT, the server should replace the entire resource with the provided data.
Ideal PUT Use Cases:
- Complete profile updates where all fields are provided
- Configuration updates that require all settings
- Resource replacement scenarios
- Bulk data modifications
PUT Request Characteristics:
- Replaces the entire resource
- Requires all fields (or uses defaults for missing ones)
- Idempotent operation
- HTTP status 200 (OK) or 204 (No Content) for success
What is a PATCH Request and Why It’s Different from PUT
PATCH requests modify only specified fields of a resource, leaving other attributes unchanged. This selective updating approach makes PATCH more efficient for scenarios where clients need to modify individual properties.
Ideal PATCH Use Cases:
- Single field updates (like changing an email address)
- Status changes (updating order status)
- Incremental modifications
- Mobile applications with limited bandwidth
PATCH Request Characteristics:
- Modifies only specified fields
- Preserves existing data for unspecified fields
- Not necessarily idempotent
- HTTP status 200 (OK) or 204 (No Content) for success
Real-World Use Cases for Each Method
Scenario | Method | Rationale |
User Profile Edit | PUT | Complete profile form with all fields |
Change Password | PATCH | Only password field needs updating |
Toggle Feature Flag | PATCH | Single boolean field modification |
Import User Data | PUT | Complete data replacement from external source |
Update Product Inventory | PATCH | Only quantity field changes |
Replace Configuration | PUT | Entire config object replacement |
Setting Up the Next.js Frontend
Creating a Next.js Project with API Integration
Initialize a new Next.js project with TypeScript support for better type safety:
npx create-next-app@latest nextjs-laravel-updates --typescript --tailwind --app
cd nextjs-laravel-updates
npm install axios react-hook-form @types/node
Project Structure Setup:
src/
├── app/
│ ├── api/
│ ├── components/
│ │ ├── forms/
│ │ └── ui/
│ ├── hooks/
│ ├── lib/
│ └── types/
API Configuration (src/lib/api.ts
):
import axios, { AxiosResponse } from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// Request interceptor for authentication
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling
api.interceptors.response.use(
(response: AxiosResponse) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
Configuring Axios or Fetch for PATCH and PUT Requests
API Service Layer (src/lib/userService.ts
):
import api from './api';
export interface User {
id: number;
name: string;
email: string;
bio?: string;
avatar?: string;
preferences?: {
notifications: boolean;
theme: 'light' | 'dark';
language: string;
};
created_at: string;
updated_at: string;
}
export interface UserUpdatePayload {
name?: string;
email?: string;
bio?: string;
avatar?: string;
preferences?: Partial<User['preferences']>;
}
class UserService {
// PUT request for complete user update
async updateUserComplete(userId: number, userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
try {
const response = await api.put(`/users/${userId}`, userData);
return response.data.data;
} catch (error) {
console.error('Complete user update failed:', error);
throw new Error('Failed to update user profile');
}
}
// PATCH request for partial user update
async updateUserPartial(userId: number, updates: UserUpdatePayload): Promise<User> {
try {
const response = await api.patch(`/users/${userId}`, updates);
return response.data.data;
} catch (error) {
console.error('Partial user update failed:', error);
throw new Error('Failed to update user information');
}
}
// Specialized PATCH for single field updates
async updateUserField(userId: number, field: keyof UserUpdatePayload, value: any): Promise<User> {
const payload = { [field]: value };
return this.updateUserPartial(userId, payload);
}
// GET request for user data
async getUser(userId: number): Promise<User> {
try {
const response = await api.get(`/users/${userId}`);
return response.data.data;
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to load user data');
}
}
}
export const userService = new UserService();
Creating Dynamic Forms to Trigger Updates
Complete Profile Update Form (src/components/forms/ProfileForm.tsx
):
'use client';
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { User, userService } from '@/lib/userService';
interface ProfileFormProps {
userId: number;
initialData?: User;
onSuccess?: (user: User) => void;
}
interface ProfileFormData {
name: string;
email: string;
bio: string;
preferences: {
notifications: boolean;
theme: 'light' | 'dark';
language: string;
};
}
export default function ProfileForm({ userId, initialData, onSuccess }: ProfileFormProps) {
const [isLoading, setIsLoading] = useState(false);
const [updateMethod, setUpdateMethod] = useState<'PUT' | 'PATCH'>('PUT');
const {
register,
handleSubmit,
formState: { errors, isDirty, dirtyFields },
reset,
watch
} = useForm<ProfileFormData>({
defaultValues: initialData ? {
name: initialData.name,
email: initialData.email,
bio: initialData.bio || '',
preferences: {
notifications: initialData.preferences?.notifications || false,
theme: initialData.preferences?.theme || 'light',
language: initialData.preferences?.language || 'en'
}
} : undefined
});
const watchedValues = watch();
const handleFormSubmit = async (data: ProfileFormData) => {
setIsLoading(true);
try {
let updatedUser: User;
if (updateMethod === 'PUT') {
// Complete update with all fields
updatedUser = await userService.updateUserComplete(userId, {
name: data.name,
email: data.email,
bio: data.bio,
preferences: data.preferences
});
} else {
// Partial update with only changed fields
const changedFields: Record<string, any> = {};
Object.keys(dirtyFields).forEach(key => {
if (key === 'preferences' && dirtyFields.preferences) {
changedFields.preferences = {};
Object.keys(dirtyFields.preferences).forEach(prefKey => {
changedFields.preferences[prefKey] = data.preferences[prefKey as keyof typeof data.preferences];
});
} else {
changedFields[key] = data[key as keyof ProfileFormData];
}
});
updatedUser = await userService.updateUserPartial(userId, changedFields);
}
onSuccess?.(updatedUser);
reset(data); // Reset form dirty state
} catch (error) {
console.error('Form submission error:', error);
alert('Failed to update profile. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Update Profile</h2>
<div className="flex gap-4 mb-4">
<label className="flex items-center">
<input
type="radio"
value="PUT"
checked={updateMethod === 'PUT'}
onChange={(e) => setUpdateMethod(e.target.value as 'PUT')}
className="mr-2"
/>
PUT (Complete Update)
</label>
<label className="flex items-center">
<input
type="radio"
value="PATCH"
checked={updateMethod === 'PATCH'}
onChange={(e) => setUpdateMethod(e.target.value as 'PATCH')}
className="mr-2"
/>
PATCH (Partial Update)
</label>
</div>
</div>
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Full Name
</label>
<input
{...register('name', { required: 'Name is required' })}
type="text"
id="name"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
type="email"
id="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="bio" className="block text-sm font-medium text-gray-700 mb-1">
Bio
</label>
<textarea
{...register('bio')}
id="bio"
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tell us about yourself..."
/>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900">Preferences</h3>
<div className="flex items-center">
<input
{...register('preferences.notifications')}
type="checkbox"
id="notifications"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="notifications" className="ml-2 block text-sm text-gray-900">
Enable email notifications
</label>
</div>
<div>
<label htmlFor="theme" className="block text-sm font-medium text-gray-700 mb-1">
Theme
</label>
<select
{...register('preferences.theme')}
id="theme"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div>
<label htmlFor="language" className="block text-sm font-medium text-gray-700 mb-1">
Language
</label>
<select
{...register('preferences.language')}
id="language"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</div>
</div>
<div className="flex justify-between items-center pt-4">
<div className="text-sm text-gray-600">
{updateMethod === 'PATCH' && isDirty && (
<span>Changed fields: {Object.keys(dirtyFields).join(', ')}</span>
)}
</div>
<button
type="submit"
disabled={isLoading || (!isDirty && updateMethod === 'PATCH')}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Updating...' : `Update Profile (${updateMethod})`}
</button>
</div>
</form>
</div>
);
}
Building the Laravel Backend for Updates
Setting Up Laravel Routes for PATCH and PUT
API Routes Configuration (routes/api.php
):
<?php
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\ProductController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/
Route::middleware('auth:sanctum')->group(function () {
// User management routes
Route::get('/user', function (Request $request) {
return $request->user();
});
// RESTful user routes with explicit PATCH and PUT
Route::get('/users/{user}', [UserController::class, 'show']);
Route::put('/users/{user}', [UserController::class, 'update']); // Complete update
Route::patch('/users/{user}', [UserController::class, 'partialUpdate']); // Partial update
// Specialized endpoints for specific updates
Route::patch('/users/{user}/preferences', [UserController::class, 'updatePreferences']);
Route::patch('/users/{user}/password', [UserController::class, 'updatePassword']);
Route::patch('/users/{user}/avatar', [UserController::class, 'updateAvatar']);
// Product management with different update strategies
Route::resource('products', ProductController::class)->except(['create', 'edit']);
Route::patch('/products/{product}/inventory', [ProductController::class, 'updateInventory']);
Route::patch('/products/{product}/status', [ProductController::class, 'updateStatus']);
});
// Public routes
Route::post('/register', [UserController::class, 'register']);
Route::post('/login', [UserController::class, 'login']);
Creating Controller Methods to Handle the Requests
User Controller (app/Http/Controllers/Api/UserController.php
):
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdateUserRequest;
use App\Http\Requests\PartialUpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class UserController extends Controller
{
/**
* Display the specified user.
*/
public function show(User $user): JsonResponse
{
$this->authorize('view', $user);
return response()->json([
'success' => true,
'data' => new UserResource($user),
'message' => 'User retrieved successfully'
]);
}
/**
* Complete user update using PUT method.
* Replaces the entire user resource with provided data.
*/
public function update(UpdateUserRequest $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validated();
// For PUT requests, we replace all updateable fields
$user->fill([
'name' => $validated['name'],
'email' => $validated['email'],
'bio' => $validated['bio'] ?? null,
]);
// Handle preferences as JSON field
if (isset($validated['preferences'])) {
$user->preferences = $validated['preferences'];
}
$user->save();
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'User updated successfully'
]);
}
/**
* Partial user update using PATCH method.
* Updates only provided fields, preserving others.
*/
public function partialUpdate(PartialUpdateUserRequest $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validated();
// Only update fields that are present in the request
if (isset($validated['name'])) {
$user->name = $validated['name'];
}
if (isset($validated['email'])) {
$user->email = $validated['email'];
}
if (isset($validated['bio'])) {
$user->bio = $validated['bio'];
}
// Handle partial preferences updates
if (isset($validated['preferences'])) {
$currentPreferences = $user->preferences ?? [];
$user->preferences = array_merge($currentPreferences, $validated['preferences']);
}
$user->save();
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'User updated successfully',
'updated_fields' => array_keys($validated)
]);
}
/**
* Update only user preferences.
*/
public function updatePreferences(Request $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validate([
'notifications' => 'boolean',
'theme' => 'string|in:light,dark',
'language' => 'string|size:2',
'timezone' => 'string|max:50'
]);
$currentPreferences = $user->preferences ?? [];
$user->preferences = array_merge($currentPreferences, $validated);
$user->save();
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'Preferences updated successfully'
]);
}
/**
* Update user password.
*/
public function updatePassword(Request $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validate([
'current_password' => 'required|string',
'password' => 'required|string|min:8|confirmed'
]);
if (!Hash::check($validated['current_password'], $user->password)) {
throw ValidationException::withMessages([
'current_password' => ['The provided password does not match your current password.']
]);
}
$user->password = Hash::make($validated['password']);
$user->save();
return response()->json([
'success' => true,
'message' => 'Password updated successfully'
]);
}
/**
* Update user avatar.
*/
public function updateAvatar(Request $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048'
]);
if ($request->hasFile('avatar')) {
// Delete old avatar if exists
if ($user->avatar) {
Storage::delete($user->avatar);
}
$avatarPath = $request->file('avatar')->store('avatars', 'public');
$user->avatar = $avatarPath;
$user->save();
}
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'Avatar updated successfully'
]);
}
}
Validating Data in Laravel for Partial and Full Updates
Complete Update Request (app/Http/Requests/UpdateUserRequest.php
):
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled in controller
}
/**
* Get the validation rules that apply to the request.
* For PUT requests, all fields are required or have defaults.
*/
public function rules(): array
{
$userId = $this->route('user')->id;
return [
'name' => 'required|string|max:255',
'email' => [
'required',
'email',
'max:255',
Rule::unique('users')->ignore($userId)
],
'bio' => 'nullable|string|max:1000',
'preferences' => 'nullable|array',
'preferences.notifications' => 'boolean',
'preferences.theme' => 'string|in:light,dark',
'preferences.language' => 'string|size:2',
'preferences.timezone' => 'string|max:50'
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.required' => 'A name is required for complete user updates.',
'email.required' => 'An email address is required for complete user updates.',
'email.unique' => 'This email address is already taken.',
'preferences.theme.in' => 'Theme must be either light or dark.',
'preferences.language.size' => 'Language code must be exactly 2 characters.'
];
}
}
Partial Update Request (app/Http/Requests/PartialUpdateUserRequest.php
):
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PartialUpdateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled in controller
}
/**
* Get the validation rules that apply to the request.
* For PATCH requests, all fields are optional.
*/
public function rules(): array
{
$userId = $this->route('user')->id;
return [
'name' => 'sometimes|required|string|max:255',
'email' => [
'sometimes',
'required',
'email',
'max:255',
Rule::unique('users')->ignore($userId)
],
'bio' => 'sometimes|nullable|string|max:1000',
'preferences' => 'sometimes|array',
'preferences.notifications' => 'sometimes|boolean',
'preferences.theme' => 'sometimes|string|in:light,dark',
'preferences.language' => 'sometimes|string|size:2',
'preferences.timezone' => 'sometimes|string|max:50'
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// Remove empty strings and null values for PATCH requests
$input = $this->all();
$cleanedInput = array_filter($input, function ($value) {
return $value !== '' && $value !== null;
});
$this->replace($cleanedInput);
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'email.unique' => 'This email address is already taken.',
'preferences.theme.in' => 'Theme must be either light or dark.',
'preferences.language.size' => 'Language code must be exactly 2 characters.'
];
}
}
Understanding RESTful Standards with PATCH vs PUT
Differences in Payload Structure and Semantics
The semantic differences between PATCH and PUT extend beyond just the fields included in requests. Understanding these nuances helps create more intuitive and maintainable APIs.
PUT Request Semantics:
// PUT /api/users/123
// Complete resource replacement
{
"name": "John Doe",
"email": "[email protected]",
"bio": "Software developer with 5 years experience",
"preferences": {
"notifications": true,
"theme": "dark",
"language": "en",
"timezone": "UTC"
}
}
PATCH Request Semantics:
// PATCH /api/users/123
// Partial resource modification
{
"preferences": {
"theme": "light"
}
}
Idempotency Explained: PUT vs PATCH in Practice
PUT Idempotency: PUT requests are idempotent by definition. Making the same PUT request multiple times should always result in the same server state.
// This PUT request will always result in the same final state
// regardless of how many times it's executed
public function update(UpdateUserRequest $request, User $user): JsonResponse
{
$validated = $request->validated();
// Complete replacement - always produces same result
$user->update([
'name' => $validated['name'],
'email' => $validated['email'],
'bio' => $validated['bio'] ?? null,
'preferences' => $validated['preferences'] ?? []
]);
return response()->json(['data' => new UserResource($user)]);
}
PATCH Idempotency Considerations: PATCH requests are not necessarily idempotent, especially when dealing with operations like incrementing values or appending to arrays.
// Non-idempotent PATCH - increments login count
public function incrementLoginCount(User $user): JsonResponse
{
$user->increment('login_count'); // Not idempotent!
return response()->json(['login_count' => $user->login_count]);
}
// Idempotent PATCH - sets specific value
public function updateLoginCount(Request $request, User $user): JsonResponse
{
$user->update(['login_count' => $request->input('login_count')]);
return response()->json(['login_count' => $user->login_count]);
}
Best Practices for RESTful Route Naming and Usage
Resource-Centric Route Design:
// Good: Resource-focused routes
Route::put('/users/{user}', [UserController::class, 'update']);
Route::patch('/users/{user}', [UserController::class, 'partialUpdate']);
// Good: Specific sub-resource updates
Route::patch('/users/{user}/preferences', [UserController::class, 'updatePreferences']);
Route::patch('/users/{user}/password', [UserController::class, 'updatePassword']);
// Avoid: Action-focused routes
Route::post('/users/{user}/change-email', [UserController::class, 'changeEmail']); // Bad
Route::post('/users/{user}/toggle-notifications', [UserController::class, 'toggleNotifications']); // Bad
HTTP Status Code Best Practices:
public function update(UpdateUserRequest $request, User $user): JsonResponse
{
$user->update($request->validated());
// Use appropriate status codes
return response()->json([
'data' => new UserResource($user)
], 200); // 200 OK for successful updates with response body
}
public function partialUpdate(PartialUpdateUserRequest $request, User $user): JsonResponse
{
$user->update($request->validated());
// Could also use 204 No Content if not returning data
return response()->json([
'data' => new UserResource($user)
], 200);
}
Making a PUT Request from Next.js
Creating a Full Data Update Function with PUT
Custom Hook for PUT Operations (src/hooks/useUserUpdate.ts
):
import { useState } from 'react';
import { User, userService, UserUpdatePayload } from '@/lib/userService';
interface UseUserUpdateResult {
updateUser: (userId: number, userData: Omit<User, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
updateUserPartial: (userId: number, updates: UserUpdatePayload) => Promise<void>;
isLoading: boolean;
error: string | null;
success: boolean;
}
export function useUserUpdate(): UseUserUpdateResult {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const updateUser = async (
userId: number,
userData: Omit<User, 'id' | 'created_at' | 'updated_at'>
) => {
setIsLoading(true);
setError(null);
setSuccess(false);
try {
await userService.updateUserComplete(userId, userData);
setSuccess(true);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to update user';
setError(errorMessage);
throw err;
} finally {
setIsLoading(false);
}
};
const updateUserPartial = async (userId: number, updates: UserUpdatePayload) => {
setIsLoading(true);
setError(null);
setSuccess(false);
try {
await userService.updateUserPartial(userId, updates);
setSuccess(true);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to update user';
setError(errorMessage);
throw err;
} finally {
setIsLoading(false);
}
};
return {
updateUser,
updateUserPartial,
isLoading,
error,
success
};
}
Connecting the Form to a PUT API Call in Laravel
Complete Profile Update Component (src/components/CompleteProfileUpdate.tsx
):
'use client';
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { User } from '@/lib/userService';
import { useUserUpdate } from '@/hooks/useUserUpdate';
interface CompleteProfileUpdateProps {
user: User;
onSuccess?: (updatedUser: User) => void;
}
interface CompleteProfileFormData {
name: string;
email: string;
bio: string;
preferences: {
notifications: boolean;
theme: 'light' | 'dark';
language: string;
timezone: string;
};
}
export default function CompleteProfileUpdate({ user, onSuccess }: CompleteProfileUpdateProps) {
const { updateUser, isLoading, error, success } = useUserUpdate();
const [showValidation, setShowValidation] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
reset,
getValues,
watch
} = useForm<CompleteProfileFormData>({
defaultValues: {
name: user.name,
email: user.email,
bio: user.bio || '',
preferences: {
notifications: user.preferences?.notifications || false,
theme: user.preferences?.theme || 'light',
language: user.preferences?.language || 'en',
timezone: user.preferences?.timezone || 'UTC'
}
}
});
// Watch all form values to show real-time payload
const watchedValues = watch();
const handleCompleteUpdate = async (data: CompleteProfileFormData) => {
setShowValidation(true);
try {
// PUT request sends ALL required fields
const completeUserData = {
name: data.name,
email: data.email,
bio: data.bio,
preferences: data.preferences
};
await updateUser(user.id, completeUserData);
// Reset form state on success
reset(data);
onSuccess?.(user);
} catch (err) {
console.error('Complete update failed:', err);
}
};
const validateRequiredFields = () => {
const values = getValues();
const missing = [];
if (!values.name?.trim()) missing.push('name');
if (!values.email?.trim()) missing.push('email');
if (!values.preferences?.theme) missing.push('theme');
if (!values.preferences?.language) missing.push('language');
return missing;
};
const missingFields = validateRequiredFields();
return (
<div className="max-w-4xl mx-auto p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Form Section */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Complete Profile Update (PUT)
</h2>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-700">{error}</p>
</div>
)}
{success && (
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-md">
<p className="text-green-700">Profile updated successfully!</p>
</div>
)}
<form onSubmit={handleSubmit(handleCompleteUpdate)} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Full Name *
</label>
<input
{...register('name', {
required: 'Name is required for complete updates',
minLength: { value: 2, message: 'Name must be at least 2 characters' }
})}
type="text"
id="name"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email Address *
</label>
<input
{...register('email', {
required: 'Email is required for complete updates',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
type="email"
id="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="bio" className="block text-sm font-medium text-gray-700 mb-1">
Bio
</label>
<textarea
{...register('bio')}
id="bio"
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tell us about yourself..."
/>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900">Preferences *</h3>
<div className="flex items-center">
<input
{...register('preferences.notifications')}
type="checkbox"
id="notifications"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="notifications" className="ml-2 block text-sm text-gray-900">
Enable email notifications
</label>
</div>
<div>
<label htmlFor="theme" className="block text-sm font-medium text-gray-700 mb-1">
Theme *
</label>
<select
{...register('preferences.theme', { required: 'Theme is required' })}
id="theme"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select theme...</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
{errors.preferences?.theme && (
<p className="mt-1 text-sm text-red-600">{errors.preferences.theme.message}</p>
)}
</div>
<div>
<label htmlFor="language" className="block text-sm font-medium text-gray-700 mb-1">
Language *
</label>
<select
{...register('preferences.language', { required: 'Language is required' })}
id="language"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select language...</option>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
{errors.preferences?.language && (
<p className="mt-1 text-sm text-red-600">{errors.preferences.language.message}</p>
)}
</div>
<div>
<label htmlFor="timezone" className="block text-sm font-medium text-gray-700 mb-1">
Timezone
</label>
<select
{...register('preferences.timezone')}
id="timezone"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="UTC">UTC</option>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="Europe/London">London</option>
<option value="Europe/Berlin">Berlin</option>
<option value="Asia/Tokyo">Tokyo</option>
</select>
</div>
</div>
<div className="flex justify-end pt-4">
<button
type="submit"
disabled={isLoading || missingFields.length > 0}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Updating...' : 'Complete Update (PUT)'}
</button>
</div>
{showValidation && missingFields.length > 0 && (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-yellow-800">
Required fields missing: {missingFields.join(', ')}
</p>
</div>
)}
</form>
</div>
{/* Payload Preview Section */}
<div className="bg-gray-50 rounded-lg shadow-md p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">
PUT Request Payload Preview
</h3>
<div className="bg-white rounded border p-4">
<pre className="text-sm text-gray-700 whitespace-pre-wrap">
{JSON.stringify({
name: watchedValues.name,
email: watchedValues.email,
bio: watchedValues.bio,
preferences: watchedValues.preferences
}, null, 2)}
</pre>
</div>
<div className="mt-4 text-sm text-gray-600">
<h4 className="font-medium mb-2">PUT Characteristics:</h4>
<ul className="list-disc list-inside space-y-1">
<li>Replaces entire resource</li>
<li>All fields must be provided</li>
<li>Idempotent operation</li>
<li>Missing fields get default values</li>
</ul>
</div>
<div className="mt-4">
<h4 className="font-medium text-sm text-gray-600 mb-2">Validation Status:</h4>
<div className="space-y-1">
{['name', 'email', 'preferences.theme', 'preferences.language'].map(field => {
const isValid = field.includes('.')
? watchedValues.preferences?.[field.split('.')[1] as keyof typeof watchedValues.preferences]
: watchedValues[field as keyof typeof watchedValues];
return (
<div key={field} className="flex items-center text-sm">
<span className={`w-2 h-2 rounded-full mr-2 ${isValid ? 'bg-green-500' : 'bg-red-500'}`}></span>
<span className={isValid ? 'text-green-700' : 'text-red-700'}>
{field}: {isValid ? 'Valid' : 'Required'}
</span>
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
);
}
Handling Success and Error Responses Gracefully
Response Handler Service (src/lib/responseHandler.ts
):
interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
errors?: Record<string, string[]>;
status?: number;
}
export class ResponseHandler {
static handleSuccess<T>(response: ApiResponse<T>, onSuccess?: (data: T) => void): void {
if (response.success && response.data) {
onSuccess?.(response.data);
// Show success notification
this.showNotification(
response.message || 'Operation completed successfully',
'success'
);
}
}
static handleError(error: any, fallbackMessage = 'An error occurred'): void {
let errorMessage = fallbackMessage;
let validationErrors: Record<string, string[]> = {};
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 422:
// Validation errors
errorMessage = data.message || 'Validation failed';
validationErrors = data.errors || {};
break;
case 401:
errorMessage = 'Unauthorized. Please log in again.';
// Redirect to login
window.location.href = '/login';
break;
case 403:
errorMessage = 'You do not have permission to perform this action.';
break;
case 404:
errorMessage = 'Resource not found.';
break;
case 500:
errorMessage = 'Server error. Please try again later.';
break;
default:
errorMessage = data.message || fallbackMessage;
}
} else if (error.request) {
errorMessage = 'Network error. Please check your connection.';
}
this.showNotification(errorMessage, 'error');
// Return error details for form handling
return {
message: errorMessage,
validationErrors
};
}
static showNotification(message: string, type: 'success' | 'error' | 'warning' | 'info'): void {
// Implementation depends on your notification system
// Could use react-hot-toast, react-toastify, or custom notifications
console.log(`[${type.toUpperCase()}] ${message}`);
// Example with browser notification
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(message, {
icon: type === 'success' ? '/icons/success.png' : '/icons/error.png'
});
}
}
static async withErrorHandling<T>(
asyncOperation: () => Promise<T>,
onSuccess?: (data: T) => void,
onError?: (error: any) => void
): Promise<T | null> {
try {
const result = await asyncOperation();
onSuccess?.(result);
return result;
} catch (error) {
const errorDetails = this.handleError(error);
onError?.(errorDetails);
return null;
}
}
}
Making a PATCH Request from Next.js
Setting Up a Partial Update Call Using PATCH
Partial Update Component (src/components/PartialProfileUpdate.tsx
):
'use client';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { User, UserUpdatePayload } from '@/lib/userService';
import { useUserUpdate } from '@/hooks/useUserUpdate';
import { ResponseHandler } from '@/lib/responseHandler';
interface PartialProfileUpdateProps {
user: User;
onSuccess?: (updatedUser: User) => void;
}
export default function PartialProfileUpdate({ user, onSuccess }: PartialProfileUpdateProps) {
const { updateUserPartial, isLoading, error, success } = useUserUpdate();
const [selectedFields, setSelectedFields] = useState<Set<string>>(new Set());
const {
register,
handleSubmit,
formState: { errors, dirtyFields },
watch,
reset
} = useForm<UserUpdatePayload>({
defaultValues: {
name: user.name,
email: user.email,
bio: user.bio || '',
preferences: user.preferences || {}
}
});
const watchedValues = watch();
const handlePartialUpdate = async (data: UserUpdatePayload) => {
// Only send fields that are either selected or dirty
const fieldsToUpdate: UserUpdatePayload = {};
if (selectedFields.has('name') || dirtyFields.name) {
fieldsToUpdate.name = data.name;
}
if (selectedFields.has('email') || dirtyFields.email) {
fieldsToUpdate.email = data.email;
}
if (selectedFields.has('bio') || dirtyFields.bio) {
fieldsToUpdate.bio = data.bio;
}
// Handle preferences separately
if (selectedFields.has('preferences') || dirtyFields.preferences) {
fieldsToUpdate.preferences = {};
if (selectedFields.has('preferences.notifications') || dirtyFields.preferences?.notifications) {
fieldsToUpdate.preferences.notifications = data.preferences?.notifications;
}
if (selectedFields.has('preferences.theme') || dirtyFields.preferences?.theme) {
fieldsToUpdate.preferences.theme = data.preferences?.theme;
}
if (selectedFields.has('preferences.language') || dirtyFields.preferences?.language) {
fieldsToUpdate.preferences.language = data.preferences?.language;
}
}
// Don't send empty objects
if (fieldsToUpdate.preferences && Object.keys(fieldsToUpdate.preferences).length === 0) {
delete fieldsToUpdate.preferences;
}
if (Object.keys(fieldsToUpdate).length === 0) {
ResponseHandler.showNotification('No fields selected for update', 'warning');
return;
}
try {
await updateUserPartial(user.id, fieldsToUpdate);
reset(data); // Reset dirty state
setSelectedFields(new Set()); // Clear selections
onSuccess?.(user);
} catch (err) {
console.error('Partial update failed:', err);
}
};
const toggleFieldSelection = (fieldPath: string) => {
const newSelection = new Set(selectedFields);
if (newSelection.has(fieldPath)) {
newSelection.delete(fieldPath);
} else {
newSelection.add(fieldPath);
}
setSelectedFields(newSelection);
};
const isFieldSelected = (fieldPath: string) => {
return selectedFields.has(fieldPath) ||
(fieldPath.includes('.') ? dirtyFields.preferences?.[fieldPath.split('.')[1] as keyof UserUpdatePayload['preferences']] : dirtyFields[fieldPath as keyof UserUpdatePayload]);
};
const getUpdatePayload = () => {
const payload: UserUpdatePayload = {};
if (isFieldSelected('name')) payload.name = watchedValues.name;
if (isFieldSelected('email')) payload.email = watchedValues.email;
if (isFieldSelected('bio')) payload.bio = watchedValues.bio;
if (isFieldSelected('preferences.notifications') ||
isFieldSelected('preferences.theme') ||
isFieldSelected('preferences.language')) {
payload.preferences = {};
if (isFieldSelected('preferences.notifications')) {
payload.preferences.notifications = watchedValues.preferences?.notifications;
}
if (isFieldSelected('preferences.theme')) {
payload.preferences.theme = watchedValues.preferences?.theme;
}
if (isFieldSelected('preferences.language')) {
payload.preferences.language = watchedValues.preferences?.language;
}
}
return payload;
};
return (
<div className="max-w-4xl mx-auto p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Form Section */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Partial Profile Update (PATCH)
</h2>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-700">{error}</p>
</div>
)}
{success && (
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-md">
<p className="text-green-700">Profile updated successfully!</p>
</div>
)}
<form onSubmit={handleSubmit(handlePartialUpdate)} className="space-y-6">
<div className="space-y-4">
{/* Name Field */}
<div className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Full Name
</label>
<div className="flex items-center">
<input
type="checkbox"
checked={isFieldSelected('name')}
onChange={() => toggleFieldSelection('name')}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
/>
<span className="text-sm text-gray-500">Include in update</span>
</div>
</div>
<input
{...register('name')}
type="text"
id="name"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isFieldSelected('name') ? 'border-blue-300 bg-blue-50' : 'border-gray-300'
}`}
/>
</div>
{/* Email Field */}
<div className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email Address
</label>
<div className="flex items-center">
<input
type="checkbox"
checked={isFieldSelected('email')}
onChange={() => toggleFieldSelection('email')}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
/>
<span className="text-sm text-gray-500">Include in update</span>
</div>
</div>
<input
{...register('email', {
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
type="email"
id="email"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isFieldSelected('email') ? 'border-blue-300 bg-blue-50' : 'border-gray-300'
}`}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
{/* Bio Field */}
<div className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<label htmlFor="bio" className="block text-sm font-medium text-gray-700">
Bio
</label>
<div className="flex items-center">
<input
type="checkbox"
checked={isFieldSelected('bio')}
onChange={() => toggleFieldSelection('bio')}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
/>
<span className="text-sm text-gray-500">Include in update</span>
</div>
</div>
<textarea
{...register('bio')}
id="bio"
rows={3}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isFieldSelected('bio') ? 'border-blue-300 bg-blue-50' : 'border-gray-300'
}`}
placeholder="Tell us about yourself..."
/>
</div>
{/* Preferences Section */}
<div className="border rounded-lg p-4">
<h3 className="text-lg font-medium text-gray-900 mb-4">Preferences</h3>
<div className="space-y-3">
{/* Notifications */}
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
{...register('preferences.notifications')}
type="checkbox"
id="notifications"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-3"
/>
<label htmlFor="notifications" className="text-sm text-gray-900">
Enable email notifications
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={isFieldSelected('preferences.notifications')}
onChange={() => toggleFieldSelection('preferences.notifications')}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
/>
<span className="text-sm text-gray-500">Include</span>
</div>
</div>
{/* Theme */}
<div className="flex items-center justify-between">
<div className="flex items-center flex-1 mr-4">
<label htmlFor="theme" className="text-sm font-medium text-gray-700 mr-3 min-w-0">
Theme
</label>
<select
{...register('preferences.theme')}
id="theme"
className={`flex-1 px-3 py-1 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isFieldSelected('preferences.theme') ? 'border-blue-300 bg-blue-50' : 'border-gray-300'
}`}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={isFieldSelected('preferences.theme')}
onChange={() => toggleFieldSelection('preferences.theme')}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
/>
<span className="text-sm text-gray-500">Include</span>
</div>
</div>
{/* Language */}
<div className="flex items-center justify-between">
<div className="flex items-center flex-1 mr-4">
<label htmlFor="language" className="text-sm font-medium text-gray-700 mr-3 min-w-0">
Language
</label>
<select
{...register('preferences.language')}
id="language"
className={`flex-1 px-3 py-1 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isFieldSelected('preferences.language') ? 'border-blue-300 bg-blue-50' : 'border-gray-300'
}`}
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={isFieldSelected('preferences.language')}
onChange={() => toggleFieldSelection('preferences.language')}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
/>
<span className="text-sm text-gray-500">Include</span>
</div>
</div>
</div>
</div>
</div>
<div className="flex justify-between items-center pt-4">
<div className="text-sm text-gray-600">
Selected fields: {Array.from(selectedFields).length + Object.keys(dirtyFields).length}
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => {
setSelectedFields(new Set());
reset();
}}
className="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
>
Clear Selection
</button>
<button
type="submit"
disabled={isLoading || (selectedFields.size === 0 && Object.keys(dirtyFields).length === 0)}
className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Updating...' : 'Partial Update (PATCH)'}
</button>
</div>
</div>
</form>
</div>
{/* Payload Preview Section */}
<div className="bg-gray-50 rounded-lg shadow-md p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">
PATCH Request Payload Preview
</h3>
<div className="bg-white rounded border p-4">
<pre className="text-sm text-gray-700 whitespace-pre-wrap">
{JSON.stringify(getUpdatePayload(), null, 2)}
</pre>
</div>
<div className="mt-4 text-sm text-gray-600">
<h4 className="font-medium mb-2">PATCH Characteristics:</h4>
<ul className="list-disc list-inside space-y-1">
<li>Updates only specified fields</li>
<li>Preserves existing data</li>
<li>Smaller payload size</li>
<li>More efficient for minor changes</li>
</ul>
</div>
<div className="mt-4">
<h4 className="font-medium text-sm text-gray-600 mb-2">Field Status:</h4>
<div className="space-y-1">
{['name', 'email', 'bio', 'preferences.notifications', 'preferences.theme', 'preferences.language'].map(field => {
const isSelected = isFieldSelected(field);
const isDirty = field.includes('.')
? dirtyFields.preferences?.[field.split('.')[1] as keyof UserUpdatePayload['preferences']]
: dirtyFields[field as keyof UserUpdatePayload];
return (
<div key={field} className="flex items-center text-sm">
<span className={`w-2 h-2 rounded-full mr-2 ${
isSelected || isDirty ? 'bg-green-500' : 'bg-gray-300'
}`}></span>
<span className={isSelected || isDirty ? 'text-green-700' : 'text-gray-500'}>
{field}: {isSelected || isDirty ? 'Will Update' : 'No Change'}
</span>
</div>
);
})}
</div>
</div>
<div className="mt-4 p-3 bg-blue-50 rounded-md">
<p className="text-sm text-blue-800">
<strong>Tip:</strong> Check boxes to manually select fields, or make changes to automatically include them.
</p>
</div>
</div>
</div>
</div>
);
}
Dynamically Sending Only Modified Fields
Smart Field Tracker Hook (src/hooks/useFieldTracker.ts
):
import { useState, useEffect, useRef } from 'react';
import { deepEqual } from '@/lib/utils';
interface UseFieldTrackerOptions<T> {
initialData: T;
onFieldChange?: (changedFields: Partial<T>) => void;
debounceMs?: number;
}
export function useFieldTracker<T extends Record<string, any>>({
initialData,
onFieldChange,
debounceMs = 300
}: UseFieldTrackerOptions<T>) {
const [currentData, setCurrentData] = useState<T>(initialData);
const [changedFields, setChangedFields] = useState<Partial<T>>({});
const timeoutRef = useRef<NodeJS.Timeout>();
const initialDataRef = useRef<T>(initialData);
useEffect(() => {
// Update initial data reference when it changes
initialDataRef.current = initialData;
setCurrentData(initialData);
setChangedFields({});
}, [initialData]);
const updateField = (fieldPath: string, value: any) => {
setCurrentData(prev => {
const newData = { ...prev };
setNestedValue(newData, fieldPath, value);
return newData;
});
// Debounce change detection
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
detectChanges(currentData, fieldPath, value);
}, debounceMs);
};
const detectChanges = (data: T, fieldPath: string, value: any) => {
const newChangedFields: Partial<T> = {};
// Check all fields for changes
Object.keys(data).forEach(key => {
const initialValue = initialDataRef.current[key];
const currentValue = data[key];
if (!deepEqual(initialValue, currentValue)) {
newChangedFields[key as keyof T] = currentValue;
}
});
setChangedFields(newChangedFields);
onFieldChange?.(newChangedFields);
};
const getChangedFieldsOnly = (): Partial<T> => {
return changedFields;
};
const hasChanges = (): boolean => {
return Object.keys(changedFields).length > 0;
};
const resetChanges = () => {
setCurrentData(initialDataRef.current);
setChangedFields({});
};
const markFieldChanged = (fieldPath: string) => {
const value = getNestedValue(currentData, fieldPath);
setChangedFields(prev => ({
...prev,
[fieldPath]: value
}));
};
return {
currentData,
changedFields,
updateField,
getChangedFieldsOnly,
hasChanges,
resetChanges,
markFieldChanged
};
}
// Utility functions for nested object manipulation
function setNestedValue(obj: any, path: string, value: any): void {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current) || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
Using Laravel’s fillable and update() Methods
Enhanced Laravel Controller with Smart Updates:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\PartialUpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class UserController extends Controller
{
/**
* Smart partial update that handles nested fields intelligently.
*/
public function partialUpdate(PartialUpdateUserRequest $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validated();
$updatedFields = [];
// Track what fields are being updated for logging
$originalData = $user->toArray();
// Handle direct field updates
$directFields = ['name', 'email', 'bio'];
foreach ($directFields as $field) {
if (array_key_exists($field, $validated)) {
$user->{$field} = $validated[$field];
$updatedFields[] = $field;
}
}
// Handle preferences with intelligent merging
if (array_key_exists('preferences', $validated)) {
$currentPreferences = $user->preferences ?? [];
$newPreferences = $validated['preferences'];
// Merge preferences, allowing null values to clear settings
$mergedPreferences = array_merge($currentPreferences, $newPreferences);
// Remove null values if needed (optional)
$mergedPreferences = array_filter($mergedPreferences, function($value) {
return $value !== null;
});
$user->preferences = $mergedPreferences;
$updatedFields[] = 'preferences';
// Log specific preference changes
$preferenceChanges = array_keys($newPreferences);
Log::info('User preferences updated', [
'user_id' => $user->id,
'changed_preferences' => $preferenceChanges,
'new_values' => $newPreferences
]);
}
// Save changes
$user->save();
// Log the update
Log::info('User partially updated', [
'user_id' => $user->id,
'updated_fields' => $updatedFields,
'request_size' => strlen(json_encode($validated)) . ' bytes'
]);
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'User updated successfully',
'updated_fields' => $updatedFields,
'changes_count' => count($updatedFields)
]);
}
/**
* Update specific user preferences with granular control.
*/
public function updatePreferences(Request $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validate([
'notifications' => 'sometimes|boolean',
'theme' => 'sometimes|string|in:light,dark,auto',
'language' => 'sometimes|string|size:2',
'timezone' => 'sometimes|string|max:50',
'dashboard_layout' => 'sometimes|string|in:grid,list,compact',
'email_frequency' => 'sometimes|string|in:daily,weekly,monthly,never'
]);
$currentPreferences = $user->preferences ?? [];
$changedSettings = [];
foreach ($validated as $key => $value) {
if (!isset($currentPreferences[$key]) || $currentPreferences[$key] !== $value) {
$changedSettings[$key] = [
'old' => $currentPreferences[$key] ?? null,
'new' => $value
];
$currentPreferences[$key] = $value;
}
}
if (empty($changedSettings)) {
return response()->json([
'success' => true,
'data' => new UserResource($user),
'message' => 'No changes detected',
'changed_settings' => []
]);
}
$user->preferences = $currentPreferences;
$user->save();
Log::info('User preferences updated', [
'user_id' => $user->id,
'changed_settings' => $changedSettings
]);
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'Preferences updated successfully',
'changed_settings' => array_keys($changedSettings)
]);
}
/**
* Bulk field update with atomic operations.
*/
public function bulkFieldUpdate(Request $request, User $user): JsonResponse
{
$this->authorize('update', $user);
$validated = $request->validate([
'updates' => 'required|array',
'updates.*.field' => 'required|string',
'updates.*.value' => 'required',
'updates.*.operation' => 'sometimes|string|in:set,increment,decrement,append,prepend'
]);
$results = [];
$user->beginTransaction();
try {
foreach ($validated['updates'] as $update) {
$field = $update['field'];
$value = $update['value'];
$operation = $update['operation'] ?? 'set';
$result = $this->applyFieldOperation($user, $field, $value, $operation);
$results[] = [
'field' => $field,
'operation' => $operation,
'success' => $result['success'],
'message' => $result['message']
];
}
$user->save();
$user->commit();
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'Bulk update completed',
'results' => $results
]);
} catch (\Exception $e) {
$user->rollback();
Log::error('Bulk update failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
'updates' => $validated['updates']
]);
return response()->json([
'success' => false,
'message' => 'Bulk update failed',
'error' => $e->getMessage()
], 500);
}
}
private function applyFieldOperation(User $user, string $field, $value, string $operation): array
{
switch ($operation) {
case 'set':
$user->{$field} = $value;
return ['success' => true, 'message' => "Field {$field} set to {$value}"];
case 'increment':
if (is_numeric($user->{$field}) && is_numeric($value)) {
$user->{$field} += $value;
return ['success' => true, 'message' => "Field {$field} incremented by {$value}"];
}
return ['success' => false, 'message' => "Cannot increment non-numeric field {$field}"];
case 'decrement':
if (is_numeric($user->{$field}) && is_numeric($value)) {
$user->{$field} -= $value;
return ['success' => true, 'message' => "Field {$field} decremented by {$value}"];
}
return ['success' => false, 'message' => "Cannot decrement non-numeric field {$field}"];
default:
return ['success' => false, 'message' => "Unknown operation: {$operation}"];
}
}
}
Security Considerations for PATCH and PUT
Protecting Routes with Middleware and Authentication
Custom Authentication Middleware (app/Http/Middleware/EnsureUserOwnership.php
):
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Models\User;
class EnsureUserOwnership
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next)
{
$user = $request->route('user');
$currentUser = $request->user();
// Admin can edit any user
if ($currentUser->hasRole('admin')) {
return $next($request);
}
// Users can only edit their own profile
if (!$user || $user->id !== $currentUser->id) {
return response()->json([
'success' => false,
'message' => 'Unauthorized. You can only update your own profile.'
], 403);
}
return $next($request);
}
}
Rate Limiting for Update Operations (app/Http/Middleware/UpdateRateLimit.php
):
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
class UpdateRateLimit
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next, int $maxAttempts = 10, int $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'success' => false,
'message' => 'Too many update attempts. Please try again in ' . $seconds . ' seconds.',
'retry_after' => $seconds
], 429);
}
RateLimiter::hit($key, $decayMinutes * 60);
$response = $next($request);
// Reset rate limit on successful update
if ($response->getStatusCode() === 200) {
RateLimiter::clear($key);
}
return $response;
}
protected function resolveRequestSignature(Request $request): string
{
return sha1(
$request->method() .
'|' . $request->route()->getName() .
'|' . $request->user()->id
);
}
}
Sanitizing Input and Preventing Mass Assignment
Advanced Input Sanitization Service:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class InputSanitizationService
{
private array $dangerousPatterns = [
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi',
'/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi',
'/javascript:/i',
'/vbscript:/i',
'/on\w+\s*=/i'
];
private array $allowedHtmlTags = ['b', 'i', 'u', 'strong', 'em', 'p', 'br'];
public function sanitizeUserInput(array $data, array $rules = []): array
{
$sanitized = [];
foreach ($data as $key => $value) {
$sanitized[$key] = $this->sanitizeValue($value, $rules[$key] ?? null);
}
return $sanitized;
}
private function sanitizeValue($value, ?string $rule = null)
{
if (is_array($value)) {
return array_map([$this, 'sanitizeValue'], $value);
}
if (!is_string($value)) {
return $value;
}
// Remove dangerous patterns
foreach ($this->dangerousPatterns as $pattern) {
$value = preg_replace($pattern, '', $value);
}
// Apply specific sanitization based on rule
switch ($rule) {
case 'html':
return strip_tags($value, '<' . implode('><', $this->allowedHtmlTags) . '>');
case 'email':
return filter_var($value, FILTER_SANITIZE_EMAIL);
case 'url':
return filter_var($value, FILTER_SANITIZE_URL);
case 'int':
return filter_var($value, FILTER_SANITIZE_NUMBER_INT);
case 'float':
return filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
default:
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
}
public function validateAgainstSchema(array $data, string $schema): array
{
$schemas = [
'user_profile' => [
'name' => 'required|string|max:255|regex:/^[a-zA-Z\s]+$/',
'email' => 'required|email|max:255',
'bio' => 'nullable|string|max:1000',
'preferences' => 'nullable|array',
'preferences.notifications' => 'boolean',
'preferences.theme' => 'string|in:light,dark',
'preferences.language' => 'string|size:2',
],
'user_partial' => [
'name' => 'sometimes|string|max:255|regex:/^[a-zA-Z\s]+$/',
'email' => 'sometimes|email|max:255',
'bio' => 'sometimes|nullable|string|max:1000',
'preferences' => 'sometimes|array',
]
];
if (!isset($schemas[$schema])) {
throw new \InvalidArgumentException("Unknown validation schema: {$schema}");
}
$validator = Validator::make($data, $schemas[$schema]);
if ($validator->fails()) {
throw new ValidationException($validator);
}
return $validator->validated();
}
public function checkForSuspiciousActivity(array $data, string $userId): bool
{
$suspiciousPatterns = [
'rapid_changes' => $this->detectRapidChanges($userId),
'bulk_sensitive_data' => $this->detectBulkSensitiveChanges($data),
'unusual_patterns' => $this->detectUnusualPatterns($data)
];
return in_array(true, $suspiciousPatterns, true);
}
private function detectRapidChanges(string $userId): bool
{
$cacheKey = "user_updates:{$userId}";
$recentUpdates = Cache::get($cacheKey, []);
// More than 5 updates in 1 minute is suspicious
$recentUpdates = array_filter($recentUpdates, function($timestamp) {
return $timestamp > time() - 60;
});
if (count($recentUpdates) > 5) {
return true;
}
$recentUpdates[] = time();
Cache::put($cacheKey, $recentUpdates, 300); // Store for 5 minutes
return false;
}
private function detectBulkSensitiveChanges(array $data): bool
{
$sensitiveFields = ['email', 'password', 'phone', 'address'];
$changedSensitiveFields = array_intersect(array_keys($data), $sensitiveFields);
// Changing multiple sensitive fields at once is suspicious
return count($changedSensitiveFields) > 2;
}
private function detectUnusualPatterns(array $data): bool
{
foreach ($data as $key => $value) {
if (is_string($value)) {
// Check for encoded scripts
if (preg_match('/(?:javascript|vbscript|data):/i', urldecode($value))) {
return true;
}
// Check for SQL injection patterns
if (preg_match('/(?:union|select|insert|update|delete|drop|create|alter)\s/i', $value)) {
return true;
}
}
}
return false;
}
}
Using CSRF Tokens or Auth Headers for Secure Communication
Enhanced API Security Middleware (app/Http/Middleware/ApiSecurityMiddleware.php
):
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
class ApiSecurityMiddleware
{
public function handle(Request $request, Closure $next)
{
// Check for valid API signature
if (!$this->validateApiSignature($request)) {
return response()->json([
'success' => false,
'message' => 'Invalid API signature'
], 401);
}
// Check for request tampering
if (!$this->validateRequestIntegrity($request)) {
return response()->json([
'success' => false,
'message' => 'Request integrity check failed'
], 400);
}
// Add security headers to response
$response = $next($request);
return $this->addSecurityHeaders($response);
}
private function validateApiSignature(Request $request): bool
{
$signature = $request->header('X-API-Signature');
$timestamp = $request->header('X-Timestamp');
$nonce = $request->header('X-Nonce');
if (!$signature || !$timestamp || !$nonce) {
return false;
}
// Check timestamp (prevent replay attacks)
if (abs(time() - $timestamp) > 300) { // 5 minutes tolerance
Log::warning('API request with expired timestamp', [
'timestamp' => $timestamp,
'current_time' => time(),
'ip' => $request->ip()
]);
return false;
}
// Validate signature
$expectedSignature = $this->generateSignature($request, $timestamp, $nonce);
return hash_equals($expectedSignature, $signature);
}
private function generateSignature(Request $request, string $timestamp, string $nonce): string
{
$payload = json_encode($request->all());
$stringToSign = $request->method() . '|' . $request->path() . '|' . $payload . '|' . $timestamp . '|' . $nonce;
return hash_hmac('sha256', $stringToSign, config('app.api_secret'));
}
private function validateRequestIntegrity(Request $request): bool
{
$contentHash = $request->header('X-Content-Hash');
if ($request->getContent() && $contentHash) {
$expectedHash = hash('sha256', $request->getContent());
return hash_equals($expectedHash, $contentHash);
}
return true; // No content to validate
}
private function addSecurityHeaders($response)
{
return $response->withHeaders([
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
'Content-Security-Policy' => "default-src 'self'",
'Referrer-Policy' => 'strict-origin-when-cross-origin'
]);
}
}
Frontend Security Implementation (src/lib/secureApiClient.ts
):
import axios, { AxiosRequestConfig } from 'axios';
import CryptoJS from 'crypto-js';
class SecureApiClient {
private apiSecret: string;
private baseURL: string;
constructor() {
this.apiSecret = process.env.NEXT_PUBLIC_API_SECRET || '';
this.baseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
}
private generateNonce(): string {
return CryptoJS.lib.WordArray.random(16).toString();
}
private generateSignature(
method: string,
path: string,
payload: string,
timestamp: string,
nonce: string
): string {
const stringToSign = `${method}|${path}|${payload}|${timestamp}|${nonce}`;
return CryptoJS.HmacSHA256(stringToSign, this.apiSecret).toString();
}
private addSecurityHeaders(config: AxiosRequestConfig): AxiosRequestConfig {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = this.generateNonce();
const payload = config.data ? JSON.stringify(config.data) : '';
const path = config.url?.replace(this.baseURL, '') || '';
const signature = this.generateSignature(
config.method?.toUpperCase() || 'GET',
path,
payload,
timestamp,
nonce
);
const contentHash = payload ? CryptoJS.SHA256(payload).toString() : '';
return {
...config,
headers: {
...config.headers,
'X-API-Signature': signature,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Content-Hash': contentHash,
'X-Requested-With': 'XMLHttpRequest'
}
};
}
async secureRequest<T>(config: AxiosRequestConfig): Promise<T> {
const secureConfig = this.addSecurityHeaders({
...config,
baseURL: this.baseURL
});
try {
const response = await axios(secureConfig);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(error.response?.data?.message || 'Request failed');
}
throw error;
}
}
async put<T>(url: string, data: any): Promise<T> {
return this.secureRequest<T>({
method: 'PUT',
url,
data
});
}
async patch<T>(url: string, data: any): Promise<T> {
return this.secureRequest<T>({
method: 'PATCH',
url,
data
});
}
}
export const secureApiClient = new SecureApiClient();
Conclusion
Implementing PATCH and PUT requests correctly between Next.js and Laravel creates a robust foundation for modern web applications that can handle complex data updates efficiently and securely. Understanding the semantic differences between these HTTP methods—PUT for complete resource replacement and PATCH for partial modifications—enables you to build more intuitive and performant APIs.
The key to successful implementation lies in choosing the right method for each use case: use PUT when you have complete data and want to ensure consistency, and use PATCH when you need to update specific fields efficiently. Laravel’s flexible validation system and Eloquent ORM make it straightforward to handle both scenarios while maintaining data integrity and security.
Summary of Key Differences and Integration Tips
Technical Implementation Differences:
- PUT requests require complete data validation and replace entire resources
- PATCH requests use partial validation with “sometimes” rules and selective field updates
- Frontend handling varies between complete form submissions (PUT) and field-specific updates (PATCH)
- Backend processing differs in validation complexity and update logic
Performance Considerations:
- PATCH requests reduce bandwidth usage and processing overhead for minor changes
- PUT requests provide better consistency guarantees and simpler logic
- Proper payload optimization can significantly impact user experience on mobile devices
- Strategic caching of partial updates can reduce server load while maintaining data freshness
- Rate limiting and request debouncing prevent abuse and improve system stability
- Field-level change tracking minimizes unnecessary database operations
Final Thoughts on Choosing PATCH vs PUT Based on Your Use Case
Choose PUT when:
- Implementing administrative interfaces that require complete data consistency
- Building data import/export functionality where entire records are replaced
- Working with configuration management where all settings must be validated together
- Handling form submissions where users provide comprehensive data updates
Choose PATCH when:
- Creating user-friendly interfaces with incremental updates (like social media profiles)
- Building mobile applications where bandwidth efficiency is crucial
- Implementing real-time collaborative features where multiple users edit different fields
- Developing APIs for third-party integrations that need surgical precision in updates
Hybrid Approach Benefits: Many successful applications implement both methods strategically:
// Example: Smart form component that chooses method based on context
const SmartUpdateForm: React.FC<SmartUpdateFormProps> = ({ user, context }) => {
const [updateStrategy, setUpdateStrategy] = useState<'PUT' | 'PATCH'>('PATCH');
useEffect(() => {
// Automatically choose strategy based on context
if (context === 'admin_panel' || context === 'data_migration') {
setUpdateStrategy('PUT');
} else if (context === 'profile_settings' || context === 'preferences') {
setUpdateStrategy('PATCH');
}
}, [context]);
const handleSubmit = async (formData: UserUpdatePayload) => {
if (updateStrategy === 'PUT') {
await userService.updateUserComplete(user.id, formData);
} else {
const changedFields = getOnlyChangedFields(formData);
await userService.updateUserPartial(user.id, changedFields);
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form implementation */}
</form>
);
};
Encouragement to Follow REST Best Practices in Full-Stack Apps
Consistent API Design: Maintain consistency across your entire application by establishing clear conventions:
// Laravel Route Conventions
Route::middleware(['auth:sanctum', 'ensure.ownership'])->group(function () {
// Complete resource management
Route::put('/users/{user}', [UserController::class, 'update']);
Route::put('/products/{product}', [ProductController::class, 'update']);
// Partial resource updates
Route::patch('/users/{user}', [UserController::class, 'partialUpdate']);
Route::patch('/products/{product}', [ProductController::class, 'partialUpdate']);
// Specific field updates
Route::patch('/users/{user}/preferences', [UserController::class, 'updatePreferences']);
Route::patch('/products/{product}/inventory', [ProductController::class, 'updateInventory']);
});
Error Handling Consistency: Implement standardized error responses that help frontend developers handle failures gracefully:
// Consistent error response format
public function handleUpdateError(\Exception $e, string $operation): JsonResponse
{
$statusCode = match(get_class($e)) {
ValidationException::class => 422,
AuthenticationException::class => 401,
AuthorizationException::class => 403,
ModelNotFoundException::class => 404,
default => 500
};
return response()->json([
'success' => false,
'message' => $e->getMessage(),
'operation' => $operation,
'error_type' => class_basename($e),
'timestamp' => now()->toISOString()
], $statusCode);
}
Documentation and Testing: Maintain comprehensive API documentation and testing suites:
// Frontend API service with built-in documentation
class DocumentedUserService {
/**
* Complete user profile update using PUT method
* @param userId - The ID of the user to update
* @param userData - Complete user data (all required fields must be provided)
* @returns Promise<User> - The updated user object
* @throws {ValidationError} When required fields are missing
* @throws {AuthorizationError} When user lacks permission
*/
async updateUserComplete(userId: number, userData: CompleteUserData): Promise<User> {
return this.apiClient.put(`/users/${userId}`, userData);
}
/**
* Partial user profile update using PATCH method
* @param userId - The ID of the user to update
* @param updates - Partial user data (only changed fields)
* @returns Promise<User> - The updated user object
* @throws {ValidationError} When provided fields have invalid values
*/
async updateUserPartial(userId: number, updates: Partial<UserData>): Promise<User> {
return this.apiClient.patch(`/users/${userId}`, updates);
}
}
Monitoring and Analytics: Track API usage patterns to optimize your implementation:
// Laravel middleware for API analytics
class ApiAnalyticsMiddleware
{
public function handle(Request $request, Closure $next)
{
$startTime = microtime(true);
$response = $next($request);
$endTime = microtime(true);
// Log API usage metrics
Log::info('API Request Analytics', [
'method' => $request->method(),
'endpoint' => $request->path(),
'user_id' => $request->user()?->id,
'response_time_ms' => round(($endTime - $startTime) * 1000, 2),
'status_code' => $response->getStatusCode(),
'payload_size' => strlen($request->getContent()),
'response_size' => strlen($response->getContent()),
'timestamp' => now()->toISOString()
]);
return $response;
}
}
Future-Proofing Your Implementation: Design your update system to accommodate future requirements:
// Extensible update service architecture
interface UpdateStrategy {
canHandle(context: UpdateContext): boolean;
execute(userId: number, data: any, context: UpdateContext): Promise<User>;
}
class PutUpdateStrategy implements UpdateStrategy {
canHandle(context: UpdateContext): boolean {
return context.requiresCompleteData || context.isAdminOperation;
}
async execute(userId: number, data: any): Promise<User> {
return userService.updateUserComplete(userId, data);
}
}
class PatchUpdateStrategy implements UpdateStrategy {
canHandle(context: UpdateContext): boolean {
return context.isPartialUpdate || context.isMobileDevice;
}
async execute(userId: number, data: any): Promise<User> {
return userService.updateUserPartial(userId, data);
}
}
class UpdateOrchestrator {
private strategies: UpdateStrategy[] = [
new PutUpdateStrategy(),
new PatchUpdateStrategy()
];
async performUpdate(userId: number, data: any, context: UpdateContext): Promise<User> {
const strategy = this.strategies.find(s => s.canHandle(context));
if (!strategy) {
throw new Error('No suitable update strategy found');
}
return strategy.execute(userId, data, context);
}
}
By following these patterns and best practices, you’ll create a maintainable, scalable full-stack application that efficiently handles data updates while providing excellent user experience. Remember that good API design is about consistency, predictability, and clear communication between your frontend and backend systems.
The investment in proper PATCH and PUT implementation pays dividends as your application grows, providing the foundation for advanced features like real-time collaboration, offline synchronization, and sophisticated caching strategies. Start with the fundamentals covered in this guide, then gradually add advanced features as your application’s requirements evolve.