Building a CRUD Application with Next.js and MySQL
In this tutorial, we’ll walk through creating a simple CRUD (Create, Read, Update, Delete) application using Next.js and MySQL. By the end, you’ll have a working CRUD application that interacts with a MySQL database.
Prerequisites
Before we start, make sure you have the following installed:
Step 1: Setting Up the Next.js Application
First, let’s create a new Next.js application. Open your terminal and run:
npx create-next-app@latest task-crud-app cd task-crud-app
Step 2: Installing Dependencies
We’ll need a few packages to connect to MySQL and manage our environment variables. Install them by running:
npm install mysql2 dotenv
Next, install Tailwind CSS and DaisyUI:
npm install tailwindcss@latest postcss@latest autoprefixer@latest daisyui
Initialize Tailwind CSS by creating the configuration files:
npx tailwindcss init -p
Step 3: Configuring Tailwind CSS and DaisyUI
Open the tailwind.config.js file and update it to include DaisyUI:
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [ require('daisyui'), ], }
Step 4: Adding Tailwind CSS to Your Project
Replace the content of your styles/globals.css file with the following to include Tailwind CSS and DaisyUI:
@tailwind base; @tailwind components; @tailwind utilities; @import "daisyui";
Step 5: Setting Up MySQL
Next, we need to set up our MySQL database. Open your MySQL client and create a new database and table:
CREATE DATABASE task_crud_db; USE task_crud_db; CREATE TABLE tasks ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
Step 6: Configuring Environment Variables
Create a .env.local file in the root of your project to store your database connection details:
DB_HOST=localhost DB_USER=root DB_PASSWORD=yourpassword DB_NAME=task_crud_db
Step 7: Connecting to MySQL
Create a new file lib/db.js and add the following code to establish a connection to your MySQL database:
import mysql from 'mysql2/promise'; const connection = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, }); export default connection;
Step 8: Creating the API Routes
Next.js API routes allow you to create endpoints to handle your CRUD operations. Create a new folder app/api/tasks and add the following files:
route.js (for fetching all tasks):
import { getConnection } from '../../../lib/db'; export async function GET(req, res) { const connection = await getConnection(); const [rows] = await connection.query('SELECT * FROM tasks'); res.status(200).json(rows); }
create/route.js (for creating a task):
import { getConnection } from '../../../../lib/db'; export async function POST(req, res) { const connection = await getConnection(); const { title, description } = await req.json(); const [result] = await connection.query('INSERT INTO tasks (title, description) VALUES (?, ?)', [title, description]); res.status(201).json({ id: result.insertId, title, description }); }
update/route.js (for updating a task):
import { getConnection } from '../../../../lib/db'; export async function PUT(req, res) { const connection = await getConnection(); const { id, title, description } = await req.json(); await connection.query('UPDATE tasks SET title = ?, description = ? WHERE id = ?', [title, description, id]); res.status(200).json({ id, title, description }); }
delete/route.js (for deleting a task):
import { getConnection } from '../../../../lib/db'; export async function DELETE(req, res) { const connection = await getConnection(); const { id } = await req.json(); await connection.query('DELETE FROM tasks WHERE id = ?', [id]); res.status(204).end(); }
Step 9: Creating the Frontend
Let’s set up the frontend components for our CRUD application. We’ll create three main components: TaskList, AddTaskForm, and the main page Page.
TaskList.js
The TaskList component will display the list of tasks and provide options to update or delete each task.
export default function TaskList({ tasks, onTaskUpdated, onTaskDeleted }) { const handleUpdate = async (task) => { const updatedTitle = prompt('Update task title', task.title); if (updatedTitle === null) return; try { const response = await fetch('/api/tasks/update', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...task, title: updatedTitle }), }); if (response.ok) { onTaskUpdated(); } else { console.error('Failed to update task'); } } catch (error) { console.error('Error:', error); } }; const handleDelete = async (id) => { if (!confirm('Are you sure you want to delete this task?')) return; try { const response = await fetch('/api/tasks/delete', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); if (response.ok) { onTaskDeleted(); } else { console.error('Failed to delete task'); } } catch (error) { console.error('Error:', error); } }; return ( <div className="flex flex-col"> <div className="-m-1.5 overflow-x-auto"> <div className="p-1.5 min-w-full inline-block align-middle"> <div className="border rounded-lg shadow overflow-hidden dark:border-neutral-700 dark:shadow-gray-900"> <table className="min-w-full divide-y divide-gray-200 dark:divide-neutral-700"> <thead className="bg-gray-50 dark:bg-neutral-700"> <tr> <th scope="col" className="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-400">Id</th> <th scope="col" className="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-400">Title</th> <th scope="col" className="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-400">Description</th> <th scope="col" className="px-6 py-3 text-end text-xs font-medium text-gray-500 uppercase dark:text-neutral-400">Actions</th> </tr> </thead> <tbody className="divide-y divide-gray-200 dark:divide-neutral-700"> {tasks.map((task) => ( <tr key={task.id}> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-neutral-200">{task.id}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200">{task.title}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200">{task.description}</td> <td className="px-6 py-4 whitespace-nowrap text-end text-sm font-medium"> <button onClick={() => handleUpdate(task)} className="px-4 py-2 font-medium text-white bg-blue-600 rounded-md hover:bg-blue-500 focus:outline-none focus:shadow-outline-blue active:bg-blue-600 transition duration-150 ease-in-out" > Update </button> <button onClick={() => handleDelete(task.id)} className="ml-2 px-4 py-2 font-medium text-white bg-red-600 rounded-md hover:bg-red-500 focus:outline-none focus:shadow-outline-red active:bg-red-600 transition duration-150 ease-in-out" > Delete </button> </td> </tr> ))} </tbody> </table> </div> </div> </div> </div> ); }
AddTaskForm.js
The AddTaskForm component will provide a form to add new tasks.
import { useState } from 'react'; export default function AddTaskForm({ onTaskAdded }) { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const handleSubmit = async (event) => { event.preventDefault(); try { const response = await fetch('/api/tasks/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, description }), }); if (response.ok) { setTitle(''); setDescription(''); onTaskAdded(); } else { console.error('Failed to add task'); } } catch (error) { console.error('Error:', error); } }; return ( <div className="flex flex-row justify-center px-10 py-10"> <div className="w-full max-w-3xl"> <h1 className="font-bold text-2xl mb-4 text-center">Add New Task</h1> <div className="flex flex-col justify-center"> <form onSubmit={handleSubmit} className="flex flex-wrap justify-center gap-4"> <div> <input type="text" placeholder="Task Title" value={title} onChange={(e) => setTitle(e.target.value)} required className="input input-bordered input-accent flex-grow" /> </div> <div> <input type="text" placeholder="Task Description" value={description} onChange={(e) => setDescription(e.target.value)} required className="input input-bordered input-accent flex-grow" /> </div> <div> <button type="submit" className="btn btn-primary"> Add Task </button> </div> </form> </div> </div> </div> ); }
Main Page (page.js)
The Page component will combine the TaskList and AddTaskForm components and manage the task state.
"use client"; import '../app/globals.css'; import { useEffect, useState } from 'react'; import AddTaskForm from './AddTaskForm'; import TaskList from './TaskList'; import Navbar from '../components/Navbar'; export default function Home() { const [tasks, setTasks] = useState([]); useEffect(() => { fetchTasks(); }, []); async function fetchTasks() { try { const response = await fetch('/api/tasks'); if (!response.ok) { throw new Error('Failed to fetch tasks'); } const data = await response.json(); setTasks(data); } catch (error) { console.error('Error fetching tasks:', error); } } function handleTaskAdded() { fetchTasks(); } function handleTaskUpdated() { fetchTasks(); } function handleTaskDeleted() { fetchTasks(); } return ( <div> <Navbar /> <AddTaskForm onTaskAdded={handleTaskAdded} /> <TaskList tasks={tasks} onTaskUpdated={handleTaskUpdated} onTaskDeleted={handleTaskDeleted} /> </div> ); }
Screenshot :
Below are the Screenshot of the Output :
You can also see the records in MySQL Workbench :
Conclusion
You now have a fully functional CRUD application using Next.js and MySQL. This simple application demonstrates how to perform basic CRUD operations with Next.js and MySQL.