Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

Handling PATCH and PUT Requests with Next.js and Laravel

Published

Next.jsLaravel

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.

Account services are unavailable until the production backend is configured.