Módulo 6·proyecto·4h
Objetivos de aprendizaje
- Diseñar e implementar un CRUD completo con Express
- Persistir datos en un archivo JSON usando fs/promises
- Usar variables de entorno con dotenv
- Implementar validaciones de entrada
- Servir un frontend estático junto con la API
REST API Profesional con Express
En este proyecto construiremos una API de gestión de tareas con CRUD completo y persistencia en un archivo JSON.
Estructura del proyecto
code
api-tareas/
├── server/
│ ├── server.js ← Servidor Express principal
│ ├── .env ← Variables de entorno
│ ├── package.json
│ └── db/
│ └── tareas.json ← "Base de datos" JSON
└── public/
└── index.html ← Frontend estático (opcional)Configuración inicial
bash
mkdir api-tareas && cd api-tareas
mkdir -p server/db public
cd server
npm init -y
npm install express dotenv
npm install --save-dev nodemonjson
// server/package.json
{
"name": "api-tareas",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"dev:native": "node --watch server.js"
}
}bash
# server/.env
PORT=3000
NODE_ENV=developmentjson
// server/db/tareas.json — archivo inicial vacío
[]El servidor completo
javascript
// server/server.js
import express from "express";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import "dotenv/config";
const app = express();
const PORT = process.env.PORT ?? 3000;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DB_FILE = path.join(__dirname, "db", "tareas.json");
const PUBLIC_DIR = path.join(__dirname, "..", "public");
// ── Middlewares ──────────────────────────────────────────────
app.use(express.json());
app.use(express.static(PUBLIC_DIR));
// Logger simple
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// ── Helpers de persistencia ──────────────────────────────────
async function leerTareas() {
const raw = await fs.readFile(DB_FILE, "utf-8");
return raw.trim() ? JSON.parse(raw) : [];
}
async function guardarTareas(tareas) {
await fs.writeFile(DB_FILE, JSON.stringify(tareas, null, 2), "utf-8");
}
// ── Rutas ────────────────────────────────────────────────────
// GET /tareas — Listar todas (con filtro opcional por estado)
app.get("/tareas", async (req, res) => {
try {
let tareas = await leerTareas();
if (req.query.estado) {
tareas = tareas.filter((t) => t.estado === req.query.estado);
}
res.json({ ok: true, total: tareas.length, datos: tareas });
} catch (err) {
res.status(500).json({ ok: false, error: "Error al leer las tareas" });
}
});
// GET /tareas/:id — Obtener una tarea
app.get("/tareas/:id", async (req, res) => {
try {
const tareas = await leerTareas();
const tarea = tareas.find((t) => t.id === req.params.id);
if (!tarea) {
return res.status(404).json({ ok: false, error: "Tarea no encontrada" });
}
res.json({ ok: true, datos: tarea });
} catch (err) {
res.status(500).json({ ok: false, error: "Error del servidor" });
}
});
// POST /tareas — Crear una nueva tarea
app.post("/tareas", async (req, res) => {
try {
const { titulo, descripcion, prioridad = "media" } = req.body;
// Validación
if (!titulo || titulo.trim().length < 3) {
return res.status(400).json({
ok: false,
error: "El título es requerido y debe tener al menos 3 caracteres",
});
}
const prioridadesValidas = ["alta", "media", "baja"];
if (!prioridadesValidas.includes(prioridad)) {
return res.status(400).json({
ok: false,
error: `Prioridad inválida. Valores permitidos: ${prioridadesValidas.join(", ")}`,
});
}
const nuevaTarea = {
id: crypto.randomUUID(),
titulo: titulo.trim(),
descripcion: descripcion?.trim() ?? "",
estado: "pendiente",
prioridad,
creadaEn: new Date().toISOString(),
actualizadaEn: new Date().toISOString(),
};
const tareas = await leerTareas();
tareas.push(nuevaTarea);
await guardarTareas(tareas);
res.status(201).json({ ok: true, datos: nuevaTarea });
} catch (err) {
res.status(500).json({ ok: false, error: "Error al crear la tarea" });
}
});
// PUT /tareas/:id — Actualizar una tarea
app.put("/tareas/:id", async (req, res) => {
try {
const tareas = await leerTareas();
const idx = tareas.findIndex((t) => t.id === req.params.id);
if (idx === -1) {
return res.status(404).json({ ok: false, error: "Tarea no encontrada" });
}
const { titulo, descripcion, estado, prioridad } = req.body;
const estadosValidos = ["pendiente", "en-progreso", "completada"];
if (estado && !estadosValidos.includes(estado)) {
return res.status(400).json({
ok: false,
error: `Estado inválido. Valores: ${estadosValidos.join(", ")}`,
});
}
// Merge — solo actualiza los campos enviados
tareas[idx] = {
...tareas[idx],
...(titulo && { titulo: titulo.trim() }),
...(descripcion !== undefined && { descripcion: descripcion.trim() }),
...(estado && { estado }),
...(prioridad && { prioridad }),
actualizadaEn: new Date().toISOString(),
};
await guardarTareas(tareas);
res.json({ ok: true, datos: tareas[idx] });
} catch (err) {
res.status(500).json({ ok: false, error: "Error al actualizar la tarea" });
}
});
// DELETE /tareas/:id — Eliminar una tarea
app.delete("/tareas/:id", async (req, res) => {
try {
const tareas = await leerTareas();
const idx = tareas.findIndex((t) => t.id === req.params.id);
if (idx === -1) {
return res.status(404).json({ ok: false, error: "Tarea no encontrada" });
}
tareas.splice(idx, 1);
await guardarTareas(tareas);
res.status(204).send();
} catch (err) {
res.status(500).json({ ok: false, error: "Error al eliminar la tarea" });
}
});
// Ruta 404 genérica
app.use((req, res) => {
res.status(404).json({ ok: false, error: `Ruta ${req.url} no encontrada` });
});
// ── Start ─────────────────────────────────────────────────────
app.listen(PORT, () => {
console.log(`🚀 API corriendo en http://localhost:${PORT}`);
console.log(`📁 DB: ${DB_FILE}`);
});Probando la API con curl
bash
# Crear tarea
curl -X POST http://localhost:3000/tareas \
-H "Content-Type: application/json" \
-d '{"titulo":"Estudiar Node.js","prioridad":"alta"}'
# Listar todas
curl http://localhost:3000/tareas
# Filtrar por estado
curl "http://localhost:3000/tareas?estado=pendiente"
# Obtener una (reemplaza el ID)
curl http://localhost:3000/tareas/abc-123
# Actualizar estado
curl -X PUT http://localhost:3000/tareas/abc-123 \
-H "Content-Type: application/json" \
-d '{"estado":"completada"}'
# Eliminar
curl -X DELETE http://localhost:3000/tareas/abc-123Endpoints de la API
| Método | Ruta | Descripción |
|---|---|---|
GET | /tareas | Listar todas (filtro: ?estado=) |
GET | /tareas/:id | Obtener una por ID |
POST | /tareas | Crear nueva tarea |
PUT | /tareas/:id | Actualizar campos |
DELETE | /tareas/:id | Eliminar |
Próximos pasos
Esta API usa un archivo JSON como "base de datos". En el módulo VII migraremos este mismo patrón a PostgreSQL con la librería pg.