Build CRUD App with Next.js and Laravel 12: Complete Full-Stack Guide - Techvblogs

Build CRUD App with Next.js and Laravel 12: Complete Full-Stack Guide

Build a complete CRUD app with Next.js frontend and Laravel 12 API backend. Step-by-step guide with code examples included.


Suresh Ramani - Author - Techvblogs
Suresh Ramani
 

6 days ago

TechvBlogs - Google News

Building a CRUD app with Next.js and Laravel 12 combines the power of modern React frontend with a robust PHP backend. This guide walks you through creating a complete full-stack application that handles Create, Read, Update, and Delete operations seamlessly.

In this tutorial, you’ll learn to build a task management system using CRUD Next.js for the frontend and CRUD Laravel 12 as the API backend. This combination gives you the best of both worlds: fast, interactive user interfaces and powerful server-side functionality.

What is a CRUD App with Next.js and Laravel 12?

CRUD Next.js and Laravel 12 application separates concerns between frontend and backend:

  • Next.js Frontend: Handles user interface, routing, and client-side interactions
  • Laravel 12 Backend: Provides API endpoints, database operations, and business logic
  • API Communication: RESTful APIs connect both applications

CRUD operations remain the same:

  • Create: Add new records
  • Read: Fetch and display data
  • Update: Modify existing records
  • Delete: Remove records

Why Choose Next.js and Laravel 12 for CRUD Apps?

Benefits of This Stack:

  • Separation of Concerns: Frontend and backend can be developed independently
  • Scalability: Each part can scale separately based on needs
  • Modern Technologies: Latest React features with proven PHP framework
  • SEO-Friendly: Next.js provides excellent SEO capabilities
  • API-First Approach: Backend can serve multiple frontends (web, mobile, etc.)

Prerequisites

Before building your CRUD app with Next.js and Laravel 12, ensure you have:

  • Node.js 18+ installed
  • PHP 8.2+ with Composer
  • MySQL or PostgreSQL database
  • Basic knowledge of React and PHP
  • Understanding of REST APIs

Part 1: Setting Up Laravel 12 Backend

Step 1: Create Laravel 12 API Project

First, let’s create our CRUD Laravel 12 backend:

composer create-project laravel/laravel task-api
cd task-api

Step 2: Configure Database

Update your .env file:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=nextjs_laravel_crud
DB_USERNAME=your_username
DB_PASSWORD=your_password

Create the database:

CREATE DATABASE nextjs_laravel_crud;

Step 3: Create Task Model and Migration

Generate the model, migration, and controller for our CRUD Laravel 12 backend:

php artisan make:model Task -mrc

Edit the migration file database/migrations/create_tasks_table.php:

<?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('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->boolean('completed')->default(false);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('tasks');
    }
};

Run the migration:

php artisan migrate

Step 4: Configure Task Model

Update app/Models/Task.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    protected $fillable = [
        'title',
        'description',
        'completed',
    ];

    protected $casts = [
        'completed' => 'boolean',
    ];
}

Step 5: Create API Controller

Update app/Http/Controllers/TaskController.php for our CRUD Laravel 12 API:

<?php

namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class TaskController extends Controller
{
    public function index(): JsonResponse
    {
        $tasks = Task::latest()->get();
        
        return response()->json([
            'success' => true,
            'data' => $tasks
        ]);
    }

    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'description' => 'nullable|string',
            'completed' => 'boolean'
        ]);

        $task = Task::create($validated);

        return response()->json([
            'success' => true,
            'data' => $task,
            'message' => 'Task created successfully'
        ], 201);
    }

    public function show(Task $task): JsonResponse
    {
        return response()->json([
            'success' => true,
            'data' => $task
        ]);
    }

    public function update(Request $request, Task $task): JsonResponse
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'description' => 'nullable|string',
            'completed' => 'boolean'
        ]);

        $task->update($validated);

        return response()->json([
            'success' => true,
            'data' => $task,
            'message' => 'Task updated successfully'
        ]);
    }

    public function destroy(Task $task): JsonResponse
    {
        $task->delete();

        return response()->json([
            'success' => true,
            'message' => 'Task deleted successfully'
        ]);
    }
}

Step 6: Install Laravel 12 API

Laravel 12 requires you to explicitly install API support:

php artisan install:api

This command will:

  • Install Laravel Sanctum for API authentication
  • Publish API routes
  • Set up necessary middleware
  • Configure API-related settings

Step 7: Set Up API Routes

Update routes/api.php:

<?php

use App\Http\Controllers\TaskController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

Route::apiResource('tasks', TaskController::class);

Step 8: Configure CORS

Since we installed the API using php artisan install:api, CORS configuration is automatically set up with Sanctum. However, you may need to update the allowed origins.

Update config/cors.php:

<?php

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://localhost:3000'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => false,
];

Step 9: Test Laravel API

Start the Laravel server:

php artisan serve

Test the API endpoints:

# Create a task
curl -X POST http://localhost:8000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Test Task", "description": "This is a test"}'

# Get all tasks
curl http://localhost:8000/api/tasks

Part 2: Setting Up Next.js Frontend

Step 1: Create Next.js Project

Create your CRUD Next.js frontend:

npx create-next-app@latest task-frontend
cd task-frontend

Choose these options:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: Yes

Step 2: Install Required Packages

Install additional packages for our CRUD Next.js app:

npm install axios react-hook-form @hookform/resolvers yup

Step 3: Create API Service

Create src/lib/api.js:

import axios from 'axios';

const API_BASE_URL = 'http://localhost:8000/api';

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export const taskApi = {
  // Get all tasks
  getTasks: async () => {
    const response = await api.get('/tasks');
    return response.data;
  },

  // Create new task
  createTask: async (taskData) => {
    const response = await api.post('/tasks', taskData);
    return response.data;
  },

  // Get single task
  getTask: async (id) => {
    const response = await api.get(`/tasks/${id}`);
    return response.data;
  },

  // Update task
  updateTask: async (id, taskData) => {
    const response = await api.put(`/tasks/${id}`, taskData);
    return response.data;
  },

  // Delete task
  deleteTask: async (id) => {
    const response = await api.delete(`/tasks/${id}`);
    return response.data;
  },
};

Step 4: Create Task Components

Create src/components/TaskForm.js:

'use client';

import { useState } from 'react';
import { useForm } from 'react-hook-form';

export default function TaskForm({ task, onSubmit, onCancel }) {
  const [isLoading, setIsLoading] = useState(false);
  
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: {
      title: task?.title || '',
      description: task?.description || '',
      completed: task?.completed || false,
    },
  });

  const onFormSubmit = async (data) => {
    setIsLoading(true);
    try {
      await onSubmit(data);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
      <div>
        <label htmlFor="title" className="block text-sm font-medium text-gray-700">
          Title
        </label>
        <input
          type="text"
          id="title"
          {...register('title', { required: 'Title is required' })}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
        />
        {errors.title && (
          <p className="mt-1 text-sm text-red-600">{errors.title.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="description" className="block text-sm font-medium text-gray-700">
          Description
        </label>
        <textarea
          id="description"
          rows={3}
          {...register('description')}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
        />
      </div>

      <div className="flex items-center">
        <input
          id="completed"
          type="checkbox"
          {...register('completed')}
          className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
        />
        <label htmlFor="completed" className="ml-2 block text-sm text-gray-900">
          Mark as completed
        </label>
      </div>

      <div className="flex justify-end space-x-3">
        <button
          type="button"
          onClick={onCancel}
          className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
        >
          Cancel
        </button>
        <button
          type="submit"
          disabled={isLoading}
          className="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
        >
          {isLoading ? 'Saving...' : (task ? 'Update' : 'Create')} Task
        </button>
      </div>
    </form>
  );
}

Create src/components/TaskList.js:

'use client';

import { useState } from 'react';

export default function TaskList({ tasks, onEdit, onDelete, onToggleComplete }) {
  const [loadingId, setLoadingId] = useState(null);

  const handleToggleComplete = async (task) => {
    setLoadingId(task.id);
    try {
      await onToggleComplete(task);
    } finally {
      setLoadingId(null);
    }
  };

  const handleDelete = async (taskId) => {
    if (window.confirm('Are you sure you want to delete this task?')) {
      setLoadingId(taskId);
      try {
        await onDelete(taskId);
      } finally {
        setLoadingId(null);
      }
    }
  };

  if (tasks.length === 0) {
    return (
      <div className="text-center py-12">
        <h3 className="mt-2 text-sm font-medium text-gray-900">No tasks</h3>
        <p className="mt-1 text-sm text-gray-500">Get started by creating a new task.</p>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {tasks.map((task) => (
        <div
          key={task.id}
          className="bg-white shadow rounded-lg p-6"
        >
          <div className="flex items-center justify-between">
            <div className="flex items-center space-x-3">
              <input
                type="checkbox"
                checked={task.completed}
                onChange={() => handleToggleComplete(task)}
                disabled={loadingId === task.id}
                className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
              />
              <div>
                <h3 className={`text-sm font-medium ${task.completed ? 'line-through text-gray-500' : 'text-gray-900'}`}>
                  {task.title}
                </h3>
                {task.description && (
                  <p className={`text-sm ${task.completed ? 'line-through text-gray-400' : 'text-gray-500'}`}>
                    {task.description}
                  </p>
                )}
              </div>
            </div>
            <div className="flex items-center space-x-2">
              <button
                onClick={() => onEdit(task)}
                className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200"
              >
                Edit
              </button>
              <button
                onClick={() => handleDelete(task.id)}
                disabled={loadingId === task.id}
                className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 disabled:opacity-50"
              >
                {loadingId === task.id ? 'Deleting...' : 'Delete'}
              </button>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

Step 5: Create Main Page

Update src/app/page.js:

'use client';

import { useState, useEffect } from 'react';
import { taskApi } from '@/lib/api';
import TaskList from '@/components/TaskList';
import TaskForm from '@/components/TaskForm';

export default function Home() {
  const [tasks, setTasks] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [showForm, setShowForm] = useState(false);
  const [editingTask, setEditingTask] = useState(null);

  useEffect(() => {
    fetchTasks();
  }, []);

  const fetchTasks = async () => {
    try {
      setLoading(true);
      const response = await taskApi.getTasks();
      setTasks(response.data);
    } catch (err) {
      setError('Failed to fetch tasks');
      console.error('Error fetching tasks:', err);
    } finally {
      setLoading(false);
    }
  };

  const handleCreateTask = async (taskData) => {
    try {
      const response = await taskApi.createTask(taskData);
      setTasks([response.data, ...tasks]);
      setShowForm(false);
    } catch (err) {
      setError('Failed to create task');
      console.error('Error creating task:', err);
    }
  };

  const handleUpdateTask = async (taskData) => {
    try {
      const response = await taskApi.updateTask(editingTask.id, taskData);
      setTasks(tasks.map(task => 
        task.id === editingTask.id ? response.data : task
      ));
      setEditingTask(null);
      setShowForm(false);
    } catch (err) {
      setError('Failed to update task');
      console.error('Error updating task:', err);
    }
  };

  const handleDeleteTask = async (taskId) => {
    try {
      await taskApi.deleteTask(taskId);
      setTasks(tasks.filter(task => task.id !== taskId));
    } catch (err) {
      setError('Failed to delete task');
      console.error('Error deleting task:', err);
    }
  };

  const handleToggleComplete = async (task) => {
    try {
      const response = await taskApi.updateTask(task.id, {
        ...task,
        completed: !task.completed
      });
      setTasks(tasks.map(t => 
        t.id === task.id ? response.data : t
      ));
    } catch (err) {
      setError('Failed to update task');
      console.error('Error updating task:', err);
    }
  };

  const handleEdit = (task) => {
    setEditingTask(task);
    setShowForm(true);
  };

  const handleCancel = () => {
    setShowForm(false);
    setEditingTask(null);
  };

  if (loading) {
    return (
      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
        <div className="text-center">
          <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-indigo-500 mx-auto"></div>
          <p className="mt-4 text-gray-600">Loading tasks...</p>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="text-center mb-8">
          <h1 className="text-3xl font-bold text-gray-900">
            Task Manager
          </h1>
          <p className="mt-2 text-gray-600">
            Built with Next.js and Laravel 12 CRUD
          </p>
        </div>

        {error && (
          <div className="mb-4 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded relative">
            {error}
            <button
              onClick={() => setError(null)}
              className="absolute top-0 bottom-0 right-0 px-4 py-3"
            >
              ×
            </button>
          </div>
        )}

        <div className="bg-white shadow rounded-lg p-6 mb-6">
          <div className="flex justify-between items-center mb-6">
            <h2 className="text-xl font-semibold text-gray-900">
              Your Tasks ({tasks.length})
            </h2>
            <button
              onClick={() => setShowForm(true)}
              className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              Add New Task
            </button>
          </div>

          {showForm && (
            <div className="mb-6 p-6 bg-gray-50 rounded-lg">
              <h3 className="text-lg font-medium text-gray-900 mb-4">
                {editingTask ? 'Edit Task' : 'Create New Task'}
              </h3>
              <TaskForm
                task={editingTask}
                onSubmit={editingTask ? handleUpdateTask : handleCreateTask}
                onCancel={handleCancel}
              />
            </div>
          )}

          <TaskList
            tasks={tasks}
            onEdit={handleEdit}
            onDelete={handleDeleteTask}
            onToggleComplete={handleToggleComplete}
          />
        </div>
      </div>
    </div>
  );
}

Step 6: Update Layout

Update src/app/layout.js:

import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Task Manager - CRUD App with Next.js and Laravel 12',
  description: 'A complete CRUD application built with Next.js frontend and Laravel 12 backend API',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

Part 3: Testing Your CRUD App

Step 1: Start Both Servers

Terminal 1 (Laravel):

cd task-api
php artisan serve

Terminal 2 (Next.js):

cd task-frontend
npm run dev

Step 2: Test CRUD Operations

Visit http://localhost:3000 and test:

  1. Create: Add new tasks using the form
  2. Read: View all tasks in the list
  3. Update: Edit existing tasks and toggle completion
  4. Delete: Remove tasks with confirmation

Advanced Features for Your CRUD Next.js Laravel 12 App

1. Add Search Functionality

Update your Laravel controller:

public function index(Request $request): JsonResponse
{
    $query = Task::query();
    
    if ($request->has('search')) {
        $query->where('title', 'like', '%' . $request->search . '%')
              ->orWhere('description', 'like', '%' . $request->search . '%');
    }
    
    $tasks = $query->latest()->get();
    
    return response()->json([
        'success' => true,
        'data' => $tasks
    ]);
}

2. Add Pagination

Implement pagination in Laravel:

public function index(Request $request): JsonResponse
{
    $tasks = Task::latest()->paginate(10);
    
    return response()->json([
        'success' => true,
        'data' => $tasks->items(),
        'pagination' => [
            'current_page' => $tasks->currentPage(),
            'last_page' => $tasks->lastPage(),
            'per_page' => $tasks->perPage(),
            'total' => $tasks->total(),
        ]
    ]);
}

3. Add Authentication

Since we already installed Laravel Sanctum with php artisan install:api, we can easily add authentication to our CRUD Laravel 12 API.

Create an authentication controller:

php artisan make:controller AuthController

Update app/Http/Controllers/AuthController.php:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function register(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        $token = $user->createToken('auth_token')->plainTextToken;

        return response()->json([
            'success' => true,
            'data' => [
                'user' => $user,
                'token' => $token,
                'token_type' => 'Bearer',
            ]
        ], 201);
    }

    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        $user = User::where('email', $request->email)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $token = $user->createToken('auth_token')->plainTextToken;

        return response()->json([
            'success' => true,
            'data' => [
                'user' => $user,
                'token' => $token,
                'token_type' => 'Bearer',
            ]
        ]);
    }
}

Add authentication routes to routes/api.php:

<?php

use App\Http\Controllers\AuthController;
use App\Http\Controllers\TaskController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
    
    Route::apiResource('tasks', TaskController::class);
});

Error Handling Best Practices

Laravel Error Handling

Create app/Http/Middleware/HandleApiErrors.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class HandleApiErrors
{
    public function handle(Request $request, Closure $next): Response
    {
        try {
            return $next($request);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'An error occurred',
                'error' => $e->getMessage()
            ], 500);
        }
    }
}

Next.js Error Handling

Create src/hooks/useErrorHandler.js:

import { useState } from 'react';

export function useErrorHandler() {
  const [error, setError] = useState(null);

  const handleError = (error) => {
    console.error('Error:', error);
    setError(error.message || 'An unexpected error occurred');
  };

  const clearError = () => setError(null);

  return { error, handleError, clearError };
}

Performance Optimization Tips

Laravel Optimization

  1. Use API Resources:
php artisan make:resource TaskResource
  1. Add Database Indexes:
$table->index(['title', 'completed']);
  1. Implement Caching:
$tasks = Cache::remember('tasks', 300, function () {
    return Task::latest()->get();
});

Next.js Optimization

  1. Use React Query for better data fetching:
npm install @tanstack/react-query
  1. Implement Loading States:
const [loading, setLoading] = useState(false);
  1. Add Error Boundaries:
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  // ... error boundary logic
}

Security Considerations

Laravel Security

  1. Rate Limiting:
Route::middleware('throttle:60,1')->group(function () {
    Route::apiResource('tasks', TaskController::class);
});
  1. Input Validation:
$validated = $request->validate([
    'title' => 'required|string|max:255',
    'description' => 'nullable|string|max:1000',
]);
  1. CORS Configuration:
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],

Next.js Security

  1. Environment Variables:
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
  1. Input Sanitization:
import DOMPurify from 'dompurify';
const cleanInput = DOMPurify.sanitize(userInput);

Deployment Guide

Deploy Laravel to Production

  1. Optimize Laravel:
php artisan config:cache
php artisan route:cache
php artisan view:cache
  1. Set Production Environment:
APP_ENV=production
APP_DEBUG=false

Deploy Next.js to Vercel

  1. Build the Application:
npm run build
  1. Configure Environment Variables in Vercel dashboard

  2. Deploy:

npx vercel --prod

Common Issues and Solutions

CORS Issues

Problem: Cross-origin requests blocked Solution: Configure CORS properly in Laravel and ensure frontend URL is allowed

API Connection Issues

Problem: Cannot connect to Laravel API Solution: Check if both servers are running and API base URL is correct

Database Connection Issues

Problem: Database connection failed Solution: Verify database credentials and ensure database server is running

Testing Your CRUD Application

Laravel Testing

Create tests/Feature/TaskApiTest.php:

<?php

namespace Tests\Feature;

use App\Models\Task;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class TaskApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_create_task()
    {
        $response = $this->postJson('/api/tasks', [
            'title' => 'Test Task',
            'description' => 'Test Description'
        ]);

        $response->assertStatus(201)
                 ->assertJson([
                     'success' => true,
                     'data' => [
                         'title' => 'Test Task'
                     ]
                 ]);
    }

    public function test_can_fetch_tasks()
    {
        Task::factory()->count(3)->create();

        $response = $this->getJson('/api/tasks');

        $response->assertStatus(200)
                 ->assertJson([
                     'success' => true
                 ]);
    }
}

Next.js Testing

Install testing libraries:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest-environment-jsdom

Create __tests__/TaskList.test.js:

import { render, screen } from '@testing-library/react';
import TaskList from '../src/components/TaskList';

const mockTasks = [
  { id: 1, title: 'Test Task', description: 'Test Description', completed: false }
];

test('renders task list', () => {
  render(
    <TaskList 
      tasks={mockTasks} 
      onEdit={() => {}} 
      onDelete={() => {}} 
      onToggleComplete={() => {}} 
    />
  );
  
  expect(screen.getByText('Test Task')).toBeInTheDocument();
});

Conclusion

Building a CRUD app with Next.js and Laravel 12 provides a powerful, scalable solution for modern web applications. This combination offers:

  • Separation of Concerns: Clear division between frontend and backend
  • Modern Technologies: Latest React and PHP frameworks
  • API-First Approach: Flexible architecture for future expansion
  • Developer Experience: Excellent tooling and development workflow

The CRUD Next.js frontend provides excellent user experience with fast, interactive interfaces, while the CRUD Laravel 12 backend offers robust API endpoints with proper validation and error handling.

Comments (0)

Comment


Note: All Input Fields are required.