CRUD con archivos JSON usando Node.js
Serie: Desarrollo de Interfaces
Curso: Desarrollo de Interfaces 1
Capítulo 17: CRUD con archivos JSON usando Node.js
Capítulo anterior: CRUD con datos en memoria
Capítulo siguiente: Introducción a APIs y consumo desde una interfaz
En el capítulo anterior construimos un CRUD con datos en memoria. Eso nos permitió entender la lógica esencial: crear, listar, editar y eliminar registros usando arreglos, objetos, formularios y eventos. Pero tenía una limitación importante: al recargar la página, los datos desaparecían.
Hoy daremos el siguiente paso: guardar los registros en un archivo JSON usando Node.js. No será todavía una base de datos completa, pero sí será persistencia real. El archivo quedará escrito en el proyecto y podremos leerlo nuevamente cuando el servidor se reinicie.
Qué aprenderás hoy
- Entender por qué un archivo JSON puede servir como primera capa de persistencia.
- Crear una estructura básica de proyecto con Node.js.
- Leer y escribir archivos con
fs/promises. - Crear rutas CRUD usando Express.
- Separar funciones de datos y rutas del servidor.
- Reconocer límites técnicos de usar JSON como almacenamiento.
Un archivo JSON no reemplaza a una base de datos, pero es excelente para aprender persistencia, practicar backend y preparar el camino hacia MySQL.
Por qué importa
Cuando una aplicación empieza a guardar datos, cambia la forma de pensar. Ya no basta con mostrar tarjetas bonitas o validar formularios. Ahora debemos preguntarnos dónde vive la información, quién puede modificarla, cómo se recupera y qué pasa si algo falla al guardar.
En proyectos de clase, un archivo JSON es una buena etapa intermedia. Permite ver los datos como texto estructurado, entender el flujo entre cliente y servidor y practicar operaciones CRUD sin configurar una base de datos desde el primer día.
Estructura del proyecto
Una estructura simple puede verse así:
crud-json-node/
data/
projects.json
server.js
package.json
El archivo projects.json será nuestra fuente de datos. server.js tendrá el servidor y las rutas. Más adelante podríamos separar el proyecto en carpetas como routes, controllers y services, pero por ahora mantendremos una versión didáctica.
Inicializar Node.js
Primero creamos el proyecto e instalamos Express:
npm init -y
npm install express
Luego podemos ajustar package.json para usar módulos modernos:
{
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.0"
}
}
La propiedad type: "module" nos permite usar import en lugar de require. Esto ayuda a mantener una sintaxis más alineada con JavaScript moderno.
Crear el archivo JSON
Dentro de la carpeta data, creamos projects.json con una lista inicial:
[
{
"id": 1,
"name": "Portafolio personal",
"team": "Valeria",
"course": "Desarrollo de Interfaces",
"status": "Publicado"
},
{
"id": 2,
"name": "Landing de producto",
"team": "Mateo y Lucía",
"course": "Desarrollo Web",
"status": "En proceso"
}
]
JSON se parece mucho a los objetos y arreglos de JavaScript, pero tiene reglas propias: las claves deben ir entre comillas dobles, no se permiten comentarios y no se admiten comas sobrantes al final.
Leer y escribir datos
Node.js permite trabajar con archivos desde el servidor. Usaremos fs/promises para leer y escribir de forma asíncrona.
import { readFile, writeFile } from "node:fs/promises";
const dataPath = new URL("./data/projects.json", import.meta.url);
async function readProjects() {
const content = await readFile(dataPath, "utf8");
return JSON.parse(content);
}
async function writeProjects(projects) {
const content = JSON.stringify(projects, null, 2);
await writeFile(dataPath, content, "utf8");
}
readFile() lee el archivo como texto. JSON.parse() convierte ese texto en datos utilizables por JavaScript. Luego, JSON.stringify() convierte el arreglo nuevamente en texto JSON para guardarlo con writeFile().
Si el JSON está mal escrito, JSON.parse() fallará. Por eso conviene validar el archivo y manejar errores cuando el proyecto crece.
Crear el servidor
Ahora preparemos un servidor mínimo con Express. Necesitamos activar express.json() para recibir datos enviados desde el cliente en formato JSON.
import express from "express";
import { readFile, writeFile } from "node:fs/promises";
const app = express();
const port = 3000;
const dataPath = new URL("./data/projects.json", import.meta.url);
app.use(express.json());
app.listen(port, function () {
console.log("Servidor iniciado en http://localhost:" + port);
});
Con esto tenemos el servidor encendido, pero todavía no hay rutas. Las rutas serán los caminos que usará la interfaz para pedir o enviar datos.
Read: listar proyectos
La primera ruta será GET /projects. Su trabajo es leer el archivo y devolver la lista completa.
app.get("/projects", async function (request, response) {
const projects = await readProjects();
response.json(projects);
});
Esta ruta reemplaza a la lista en memoria del capítulo anterior. La diferencia es que ahora los datos vienen del archivo.
Create: agregar un proyecto
Para crear un proyecto usamos POST /projects. Leemos los proyectos existentes, calculamos el siguiente id, agregamos el nuevo registro y guardamos el archivo.
app.post("/projects", async function (request, response) {
const projects = await readProjects();
const nextId = projects.length === 0
? 1
: Math.max(...projects.map(function (project) {
return project.id;
})) + 1;
const newProject = {
id: nextId,
name: request.body.name,
team: request.body.team,
course: request.body.course,
status: request.body.status
};
projects.push(newProject);
await writeProjects(projects);
response.status(201).json(newProject);
});
El cliente no debería decidir el id. Esa responsabilidad pertenece al servidor, porque el servidor conoce todos los registros existentes.
Update: modificar un proyecto
Para actualizar, necesitamos recibir un identificador en la URL. Por ejemplo: PUT /projects/2.
app.put("/projects/:id", async function (request, response) {
const id = Number(request.params.id);
const projects = await readProjects();
const index = projects.findIndex(function (project) {
return project.id === id;
});
if (index === -1) {
response.status(404).json({ message: "Proyecto no encontrado" });
return;
}
projects[index] = {
id: id,
name: request.body.name,
team: request.body.team,
course: request.body.course,
status: request.body.status
};
await writeProjects(projects);
response.json(projects[index]);
});
La lógica se parece mucho al CRUD en memoria. La diferencia es que antes de modificar leemos el archivo, y después de modificar volvemos a guardarlo.
Delete: eliminar un proyecto
Para eliminar, podemos crear una nueva lista sin el proyecto indicado:
app.delete("/projects/:id", async function (request, response) {
const id = Number(request.params.id);
const projects = await readProjects();
const newProjects = projects.filter(function (project) {
return project.id !== id;
});
if (newProjects.length === projects.length) {
response.status(404).json({ message: "Proyecto no encontrado" });
return;
}
await writeProjects(newProjects);
response.json({ message: "Proyecto eliminado" });
});
Esta ruta muestra una buena práctica: si no se eliminó nada, devolvemos un estado 404. Así la interfaz puede informar correctamente lo ocurrido.
Variante profesional: separar responsabilidades
Aunque el ejemplo funciona en un solo archivo, conviene pensar en capas. Las funciones readProjects() y writeProjects() pertenecen a la capa de datos. Las rutas pertenecen a la capa HTTP. Si mezclamos demasiado, el código crece rápido y se vuelve difícil de mantener.
Una mejora natural sería crear un archivo project-store.js para las funciones de lectura y escritura. Después, server.js solo se encargaría de recibir solicitudes y devolver respuestas.
Límites de usar JSON como almacenamiento
Un archivo JSON es útil para aprender, prototipar y trabajar proyectos pequeños. Pero tiene límites importantes. Si muchas personas escriben al mismo tiempo, pueden aparecer conflictos. Si el archivo crece demasiado, leerlo y escribirlo completo en cada operación se vuelve ineficiente. Y si necesitamos búsquedas complejas, relaciones o seguridad avanzada, una base de datos será mejor opción.
Usa JSON para aprender persistencia y construir prototipos. Cuando el proyecto tenga usuarios reales, concurrencia o datos críticos, migra a una base de datos.
Errores comunes
- Guardar datos en memoria y creer que ya están persistidos.
- Modificar el arreglo pero olvidar llamar a
writeProjects(). - Permitir que el cliente envíe el
idfinal del registro. - No validar los datos antes de guardarlos.
- No manejar errores cuando el archivo JSON está vacío o mal formado.
- Usar JSON como si fuera una base de datos para proyectos grandes.
Reto práctico
Construye un CRUD con JSON para registrar proyectos de clase. Cada proyecto debe tener id, name, team, course, status y createdAt. Crea rutas para listar, crear, editar y eliminar.
Variante extra: agrega una ruta GET /projects/status/:status para filtrar proyectos por estado. Luego conecta esa ruta con una interfaz HTML usando fetch().
Conclusión
Guardar datos en un archivo JSON marca una transición importante. Ya no estamos trabajando solo con elementos visuales ni con datos temporales. Ahora la aplicación tiene memoria entre sesiones y empieza a parecerse a un sistema real.
Este paso también ayuda a entender el papel del backend: recibir solicitudes, modificar datos, devolver respuestas y proteger la lógica de almacenamiento. La interfaz sigue siendo fundamental, pero ya no trabaja sola. Empieza a dialogar con un servidor.
En el siguiente capítulo conectaremos esta idea con el consumo desde la interfaz: usaremos fetch() para que una página HTML pueda comunicarse con las rutas del servidor y mostrar datos reales.