Building a CRUD Application with Next.js and MySQL

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top