Aprende a crear un sistema CRUD completo con Next.js y MySQL paso a paso. Esta guía usa JavaScript puro con ejemplos sencillos y el menor código posible.
# Crear proyecto Next.js
npx create-next-app@latest mi-crud-app
cd mi-crud-app
# Instalar dependencias para MySQL
npm install mysql2
npm install @next/env
# Instalar dependencias adicionales
npm install axios mi-crud-app/
├── app/
│ ├── api/
│ │ └── users/
│ │ ├── route.js
│ │ └── [id]/
│ │ └── route.js
│ ├── users/
│ │ ├── page.js
│ │ ├── create/
│ │ │ └── page.js
│ │ └── edit/
│ │ └── [id]/
│ │ └── page.js
│ ├── globals.css
│ ├── layout.js
│ └── page.js
├── lib/
│ └── db.js
├── components/
│ ├── UserForm.js
│ └── UserList.js
└── .env.local # .env.local
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=tu_password
DB_NAME=crud_nextjs // lib/db.js
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,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
})
export default connection -- Ejecutar en MySQL
CREATE DATABASE crud_nextjs;
USE crud_nextjs;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insertar datos de prueba
INSERT INTO users (name, email, age) VALUES
('Juan Pérez', 'juan@email.com', 25),
('María García', 'maria@email.com', 30),
('Carlos López', 'carlos@email.com', 28); Asegúrate de tener MySQL instalado y corriendo en tu sistema. Crea la base de datos antes de ejecutar la aplicación.
// app/api/users/route.js
import db from '@/lib/db'
import { NextResponse } from 'next/server'
// GET - Obtener todos los usuarios
export async function GET() {
try {
const [rows] = await db.execute('SELECT * FROM users ORDER BY created_at DESC')
return NextResponse.json(rows)
} catch (error) {
return NextResponse.json(
{ error: 'Error al obtener usuarios' },
{ status: 500 }
)
}
}
// POST - Crear nuevo usuario
export async function POST(request) {
try {
const { name, email, age } = await request.json()
const [result] = await db.execute(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
[name, email, age]
)
return NextResponse.json(
{ id: result.insertId, name, email, age },
{ status: 201 }
)
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
return NextResponse.json(
{ error: 'El email ya existe' },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Error al crear usuario' },
{ status: 500 }
)
}
} // app/api/users/[id]/route.js
import db from '@/lib/db'
import { NextResponse } from 'next/server'
// GET - Obtener usuario por ID
export async function GET(request, { params }) {
try {
const [rows] = await db.execute('SELECT * FROM users WHERE id = ?', [params.id])
if (rows.length === 0) {
return NextResponse.json(
{ error: 'Usuario no encontrado' },
{ status: 404 }
)
}
return NextResponse.json(rows[0])
} catch (error) {
return NextResponse.json(
{ error: 'Error al obtener usuario' },
{ status: 500 }
)
}
}
// PUT - Actualizar usuario
export async function PUT(request, { params }) {
try {
const { name, email, age } = await request.json()
const [result] = await db.execute(
'UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?',
[name, email, age, params.id]
)
if (result.affectedRows === 0) {
return NextResponse.json(
{ error: 'Usuario no encontrado' },
{ status: 404 }
)
}
return NextResponse.json({ id: params.id, name, email, age })
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
return NextResponse.json(
{ error: 'El email ya existe' },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Error al actualizar usuario' },
{ status: 500 }
)
}
}
// DELETE - Eliminar usuario
export async function DELETE(request, { params }) {
try {
const [result] = await db.execute('DELETE FROM users WHERE id = ?', [params.id])
if (result.affectedRows === 0) {
return NextResponse.json(
{ error: 'Usuario no encontrado' },
{ status: 404 }
)
}
return NextResponse.json({ message: 'Usuario eliminado correctamente' })
} catch (error) {
return NextResponse.json(
{ error: 'Error al eliminar usuario' },
{ status: 500 }
)
}
} // components/UserList.js
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
export default function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchUsers()
}, [])
const fetchUsers = async () => {
try {
const res = await fetch('/api/users')
const data = await res.json()
setUsers(data)
} catch (error) {
console.error('Error:', error)
} finally {
setLoading(false)
}
}
const deleteUser = async (id) => {
if (!confirm('¿Estás seguro de eliminar este usuario?')) return
try {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' })
if (res.ok) {
setUsers(users.filter(user => user.id !== id))
}
} catch (error) {
console.error('Error:', error)
}
}
if (loading) return <div className="text-center py-4">Cargando...</div>
return (
<div className="max-w-4xl mx-auto p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Lista de Usuarios</h1>
<Link
href="/users/create"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Nuevo Usuario
</Link>
</div>
<div className="bg-white shadow rounded-lg overflow-hidden">
<table className="min-w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Nombre
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Edad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Acciones
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{user.id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{user.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{user.email}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{user.age}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<Link
href={`/users/edit/${user.id}`}
className="text-blue-600 hover:text-blue-900 mr-4"
>
Editar
</Link>
<button
onClick={() => deleteUser(user.id)}
className="text-red-600 hover:text-red-900"
>
Eliminar
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
} // components/UserForm.js
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function UserForm({ user = null, isEdit = false }) {
const router = useRouter()
const [formData, setFormData] = useState({
name: user?.name || '',
email: user?.email || '',
age: user?.age || ''
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError('')
try {
const url = isEdit ? `/api/users/${user.id}` : '/api/users'
const method = isEdit ? 'PUT' : 'POST'
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
const data = await res.json()
if (res.ok) {
router.push('/users')
} else {
setError(data.error || 'Error al guardar usuario')
}
} catch (error) {
setError('Error de conexión')
} finally {
setLoading(false)
}
}
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">
{isEdit ? 'Editar Usuario' : 'Crear Usuario'}
</h1>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nombre
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Edad
</label>
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
min="1"
max="120"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex space-x-4">
<button
type="submit"
disabled={loading}
className="flex-1 bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Guardando...' : (isEdit ? 'Actualizar' : 'Crear')}
</button>
<button
type="button"
onClick={() => router.push('/users')}
className="flex-1 bg-gray-500 text-white py-2 px-4 rounded-md hover:bg-gray-600"
>
Cancelar
</button>
</div>
</form>
</div>
)
} // app/users/page.js
import UserList from '@/components/UserList'
export default function UsersPage() {
return <UserList />
} // app/users/create/page.js
import UserForm from '@/components/UserForm'
export default function CreateUserPage() {
return <UserForm />
} // app/users/edit/[id]/page.js
async function getUser(id) {
try {
const res = await fetch(`http://localhost:3000/api/users/${id}`, {
cache: 'no-store'
})
if (!res.ok) return null
return await res.json()
} catch (error) {
return null
}
}
export default async function EditUserPage({ params }) {
const user = await getUser(params.id)
if (!user) {
return (
<div className="max-w-md mx-auto p-6 text-center">
<h1 className="text-2xl font-bold text-red-600">Usuario no encontrado</h1>
</div>
)
}
return <UserForm user={user} isEdit={true} />
} // app/page.js
import Link from 'next/link'
export default function HomePage() {
return (
<div className="min-h-screen bg-gray-100">
<div className="max-w-4xl mx-auto py-12 px-6">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-8">
CRUD con Next.js y MySQL
</h1>
<p className="text-xl text-gray-600 mb-12">
Sistema completo de gestión de usuarios
</p>
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-semibold mb-6">Funcionalidades</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="text-left">
<h3 className="font-semibold text-lg mb-2">✅ Crear Usuarios</h3>
<p className="text-gray-600">Agregar nuevos usuarios con validación</p>
</div>
<div className="text-left">
<h3 className="font-semibold text-lg mb-2">📋 Listar Usuarios</h3>
<p className="text-gray-600">Ver todos los usuarios en tabla</p>
</div>
<div className="text-left">
<h3 className="font-semibold text-lg mb-2">✏️ Editar Usuarios</h3>
<p className="text-gray-600">Actualizar información existente</p>
</div>
<div className="text-left">
<h3 className="font-semibold text-lg mb-2">🗑️ Eliminar Usuarios</h3>
<p className="text-gray-600">Borrar usuarios con confirmación</p>
</div>
</div>
<Link
href="/users"
className="inline-block bg-blue-500 text-white px-8 py-3 rounded-lg text-lg font-semibold hover:bg-blue-600 transition-colors"
>
Ver Usuarios
</Link>
</div>
</div>
</div>
</div>
)
} // app/layout.js
import './globals.css'
export const metadata = {
title: 'CRUD Next.js MySQL',
description: 'Sistema CRUD con Next.js y MySQL',
}
export default function RootLayout({ children }) {
return (
<html lang="es">
<body>
<nav className="bg-blue-600 text-white p-4">
<div className="max-w-4xl mx-auto flex justify-between items-center">
<h1 className="text-xl font-bold">
<a href="/">CRUD App</a>
</h1>
<div className="space-x-4">
<a href="/" className="hover:text-blue-200">Inicio</a>
<a href="/users" className="hover:text-blue-200">Usuarios</a>
<a href="/users/create" className="hover:text-blue-200">Crear Usuario</a>
</div>
</div>
</nav>
<main className="min-h-screen bg-gray-100">
{children}
</main>
</body>
</html>
)
} /* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
} # Instalar dependencias
npm install
# Ejecutar en desarrollo
npm run dev
# Abrir en el navegador
# http://localhost:3000 1. Ir a http://localhost:3000
2. Hacer clic en "Ver Usuarios"
3. Crear un nuevo usuario
4. Editar usuario existente
5. Eliminar usuario
6. Verificar datos en MySQL Este CRUD usa conexión pool de MySQL para mejor rendimiento. Las consultas están optimizadas y usan prepared statements para seguridad.
Para producción, agrega validación en el servidor, paginación en la lista de usuarios y manejo de errores más robusto.
Recuerda configurar las variables de entorno correctamente y tener MySQL corriendo antes de ejecutar la aplicación.