CODERWEEN 🎃

Aprovecha 50% OFF en CURSOS y CARRERAS

|

Hasta el 04/11 ⏰

CODERWEEN 🎃

Aprovecha 50% OFF en CURSOS y CARRERAS

|

Hasta el 04/11 ⏰

Hasta el 04/11 ⏰

CODERWEEN 🎃

Aprovecha 50% OFF en CURSOS y CARRERAS

Cómo crear una API REST con Node.js y Express paso a paso: tu primer proyecto backend conectado a una base de datos

Guía gratuita de empleabilidad

2026
Descargar la guía

Guía gratuita: IA en tu equipo

2026
Descargar la guía

Dan Patiño

AI Strategy & Innovation en Coderhouse

Programación y Desarrollo Web

Cómo crear una API REST con Node.js y Express paso a paso: tu primer proyecto backend conectado a una base de datos

Cómo crear una API REST con Node.js y Express paso a paso: tu primer proyecto backend conectado a una base de datos

Publicado el

Resumen ejecutivo

  • En este tutorial construís desde cero una API REST de tareas con Node.js y Express, con los cinco endpoints básicos: listar, ver uno, crear, editar y borrar.

  • Vas a aprender a organizar rutas, leer datos en JSON, validar lo que llega y responder con los códigos de estado HTTP correctos.

  • Primero guardamos los datos en memoria para entender el flujo, y después los pasamos a una base de datos SQLite para que no se pierdan.

  • Terminado y subido a GitHub con un buen README, es un proyecto backend válido para tu portfolio.

Casi todas las aplicaciones que usás a diario, desde una app de delivery hasta tu home banking, se comunican con un servidor a través de una API. Saber construir una es la habilidad base de cualquier desarrollador backend, y Node.js con Express es una de las combinaciones más usadas para aprenderlo: es JavaScript, tiene muchísima documentación y te permite tener algo funcionando en pocos minutos. Si todavía no tenés claro qué es Node.js, empezá por Node.js desde cero. Y si querés el concepto de API explicado sin código, leé API REST explicadas para no técnicos.

Qué vas a construir

Una API para gestionar una lista de tareas. Cada tarea tiene un id, un titulo y un campo completada. Los endpoints serán:

  • GET /tareas: lista todas las tareas.

  • GET /tareas/:id: devuelve una tarea.

  • POST /tareas: crea una tarea.

  • PUT /tareas/:id: actualiza una tarea.

  • DELETE /tareas/:id: borra una tarea.

Paso 1: preparar el proyecto

Necesitás tener Node.js instalado (la documentación oficial de Node.js explica cómo). Luego, en una terminal:

mkdir api-tareas
cd api-tareas
npm init -y
npm install express
mkdir api-tareas
cd api-tareas
npm init -y
npm install express
mkdir api-tareas
cd api-tareas
npm init -y
npm install express
mkdir api-tareas
cd api-tareas
npm init -y
npm install express

Esto crea un archivo package.json y descarga Express. Creá un archivo llamado index.js.

Paso 2: tu primer servidor con Express

Siguiendo la estructura del ejemplo inicial de Express, escribí en index.js:

const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ mensaje: 'API de tareas funcionando' });
});

app.listen(PORT, () => {
  console.log(`Servidor en http://localhost:${PORT}`);
});
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ mensaje: 'API de tareas funcionando' });
});

app.listen(PORT, () => {
  console.log(`Servidor en http://localhost:${PORT}`);
});
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ mensaje: 'API de tareas funcionando' });
});

app.listen(PORT, () => {
  console.log(`Servidor en http://localhost:${PORT}`);
});
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ mensaje: 'API de tareas funcionando' });
});

app.listen(PORT, () => {
  console.log(`Servidor en http://localhost:${PORT}`);
});

Ejecutá node index.js y abrí http://localhost:3000 en el navegador. Si ves el mensaje en JSON, tu servidor funciona. La línea app.use(express.json()) le dice a Express que lea el cuerpo de las peticiones en formato JSON; sin ella, no vas a poder recibir datos.

Paso 3: los datos en memoria

Para entender el flujo sin sumar una base de datos todavía, guardamos las tareas en un arreglo. Agregá debajo de app.use:

let tareas = [
  { id: 1, titulo: 'Aprender Express', completada: false },
  { id: 2, titulo: 'Crear mi primera API', completada: false }
];
let siguienteId = 3;
let tareas = [
  { id: 1, titulo: 'Aprender Express', completada: false },
  { id: 2, titulo: 'Crear mi primera API', completada: false }
];
let siguienteId = 3;
let tareas = [
  { id: 1, titulo: 'Aprender Express', completada: false },
  { id: 2, titulo: 'Crear mi primera API', completada: false }
];
let siguienteId = 3;
let tareas = [
  { id: 1, titulo: 'Aprender Express', completada: false },
  { id: 2, titulo: 'Crear mi primera API', completada: false }
];
let siguienteId = 3;

Ojo: estos datos se borran cada vez que reiniciás el servidor. Lo resolvemos en el paso 8.

Paso 4: endpoints para leer (GET)

app.get('/tareas', (req, res) => {
  res.json(tareas);
});

app.get('/tareas/:id', (req, res) => {
  const id = Number(req.params.id);
  const tarea = tareas.find(t => t.id === id);
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  res.json(tarea);
});
app.get('/tareas', (req, res) => {
  res.json(tareas);
});

app.get('/tareas/:id', (req, res) => {
  const id = Number(req.params.id);
  const tarea = tareas.find(t => t.id === id);
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  res.json(tarea);
});
app.get('/tareas', (req, res) => {
  res.json(tareas);
});

app.get('/tareas/:id', (req, res) => {
  const id = Number(req.params.id);
  const tarea = tareas.find(t => t.id === id);
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  res.json(tarea);
});
app.get('/tareas', (req, res) => {
  res.json(tareas);
});

app.get('/tareas/:id', (req, res) => {
  const id = Number(req.params.id);
  const tarea = tareas.find(t => t.id === id);
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  res.json(tarea);
});

El :id en la ruta es un parámetro: Express lo deja disponible en req.params. La guía de rutas de Express explica todas las variantes. Fijate que si la tarea no existe respondemos con el código 404, no con un 200 vacío.

Paso 5: crear una tarea (POST) con validación

app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const nueva = { id: siguienteId++, titulo, completada: false };
  tareas.push(nueva);
  res.status(201).json(nueva);
});
app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const nueva = { id: siguienteId++, titulo, completada: false };
  tareas.push(nueva);
  res.status(201).json(nueva);
});
app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const nueva = { id: siguienteId++, titulo, completada: false };
  tareas.push(nueva);
  res.status(201).json(nueva);
});
app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const nueva = { id: siguienteId++, titulo, completada: false };
  tareas.push(nueva);
  res.status(201).json(nueva);
});

Dos detalles que separan una API prolija de una improvisada: validamos que llegue el título (si no, respondemos 400) y, si todo sale bien, devolvemos 201, que significa «recurso creado».

Paso 6: editar y borrar (PUT y DELETE)

app.put('/tareas/:id', (req, res) => {
  const tarea = tareas.find(t => t.id === Number(req.params.id));
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  const { titulo, completada } = req.body;
  if (titulo !== undefined) tarea.titulo = titulo;
  if (completada !== undefined) tarea.completada = Boolean(completada);
  res.json(tarea);
});

app.delete('/tareas/:id', (req, res) => {
  const existe = tareas.some(t => t.id === Number(req.params.id));
  if (!existe) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  tareas = tareas.filter(t => t.id !== Number(req.params.id));
  res.status(204).send();
});
app.put('/tareas/:id', (req, res) => {
  const tarea = tareas.find(t => t.id === Number(req.params.id));
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  const { titulo, completada } = req.body;
  if (titulo !== undefined) tarea.titulo = titulo;
  if (completada !== undefined) tarea.completada = Boolean(completada);
  res.json(tarea);
});

app.delete('/tareas/:id', (req, res) => {
  const existe = tareas.some(t => t.id === Number(req.params.id));
  if (!existe) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  tareas = tareas.filter(t => t.id !== Number(req.params.id));
  res.status(204).send();
});
app.put('/tareas/:id', (req, res) => {
  const tarea = tareas.find(t => t.id === Number(req.params.id));
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  const { titulo, completada } = req.body;
  if (titulo !== undefined) tarea.titulo = titulo;
  if (completada !== undefined) tarea.completada = Boolean(completada);
  res.json(tarea);
});

app.delete('/tareas/:id', (req, res) => {
  const existe = tareas.some(t => t.id === Number(req.params.id));
  if (!existe) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  tareas = tareas.filter(t => t.id !== Number(req.params.id));
  res.status(204).send();
});
app.put('/tareas/:id', (req, res) => {
  const tarea = tareas.find(t => t.id === Number(req.params.id));
  if (!tarea) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  const { titulo, completada } = req.body;
  if (titulo !== undefined) tarea.titulo = titulo;
  if (completada !== undefined) tarea.completada = Boolean(completada);
  res.json(tarea);
});

app.delete('/tareas/:id', (req, res) => {
  const existe = tareas.some(t => t.id === Number(req.params.id));
  if (!existe) {
    return res.status(404).json({ error: 'Tarea no encontrada' });
  }
  tareas = tareas.filter(t => t.id !== Number(req.params.id));
  res.status(204).send();
});

El 204 indica que la operación salió bien y no hay contenido para devolver.

Paso 7: probar la API

Las peticiones GET se prueban desde el navegador, pero para POST, PUT y DELETE necesitás una herramienta como Postman, Insomnia, la extensión Thunder Client de VS Code o la terminal con curl:

curl -X POST http://localhost:3000/tareas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Probar mi API"}'
curl -X POST http://localhost:3000/tareas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Probar mi API"}'
curl -X POST http://localhost:3000/tareas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Probar mi API"}'
curl -X POST http://localhost:3000/tareas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Probar mi API"}'

Probá también los casos de error: crear una tarea sin título o pedir un id que no existe. Si te responde 400 y 404, vas bien.

Códigos de estado que usaste

  • 200 OK: todo salió bien y hay datos en la respuesta.

  • 201 Created: se creó un recurso nuevo.

  • 204 No Content: salió bien, sin contenido para devolver.

  • 400 Bad Request: los datos enviados son inválidos.

  • 404 Not Found: el recurso no existe.

La referencia de códigos de estado HTTP de MDN tiene la lista completa en español.

Paso 8: conectar una base de datos con SQLite

Para que las tareas no se pierdan al reiniciar, las guardamos en SQLite, una base de datos que vive en un solo archivo y no requiere instalar un servidor. Instalá la librería better-sqlite3:

npm install better-sqlite3
npm install better-sqlite3
npm install better-sqlite3
npm install better-sqlite3

Reemplazá el arreglo en memoria por esta configuración al principio de index.js:

const Database = require('better-sqlite3');
const db = new Database('tareas.db');

db.exec(`CREATE TABLE IF NOT EXISTS tareas (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titulo TEXT NOT NULL,
  completada INTEGER NOT NULL DEFAULT 0
)`);
const Database = require('better-sqlite3');
const db = new Database('tareas.db');

db.exec(`CREATE TABLE IF NOT EXISTS tareas (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titulo TEXT NOT NULL,
  completada INTEGER NOT NULL DEFAULT 0
)`);
const Database = require('better-sqlite3');
const db = new Database('tareas.db');

db.exec(`CREATE TABLE IF NOT EXISTS tareas (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titulo TEXT NOT NULL,
  completada INTEGER NOT NULL DEFAULT 0
)`);
const Database = require('better-sqlite3');
const db = new Database('tareas.db');

db.exec(`CREATE TABLE IF NOT EXISTS tareas (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titulo TEXT NOT NULL,
  completada INTEGER NOT NULL DEFAULT 0
)`);

Y adaptá los endpoints para leer y escribir en la tabla. Por ejemplo, listar y crear quedan así:

app.get('/tareas', (req, res) => {
  const filas = db.prepare('SELECT * FROM tareas').all();
  res.json(filas);
});

app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const info = db.prepare('INSERT INTO tareas (titulo) VALUES (?)').run(titulo);
  const nueva = db.prepare('SELECT * FROM tareas WHERE id = ?').get(info.lastInsertRowid);
  res.status(201).json(nueva);
});
app.get('/tareas', (req, res) => {
  const filas = db.prepare('SELECT * FROM tareas').all();
  res.json(filas);
});

app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const info = db.prepare('INSERT INTO tareas (titulo) VALUES (?)').run(titulo);
  const nueva = db.prepare('SELECT * FROM tareas WHERE id = ?').get(info.lastInsertRowid);
  res.status(201).json(nueva);
});
app.get('/tareas', (req, res) => {
  const filas = db.prepare('SELECT * FROM tareas').all();
  res.json(filas);
});

app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const info = db.prepare('INSERT INTO tareas (titulo) VALUES (?)').run(titulo);
  const nueva = db.prepare('SELECT * FROM tareas WHERE id = ?').get(info.lastInsertRowid);
  res.status(201).json(nueva);
});
app.get('/tareas', (req, res) => {
  const filas = db.prepare('SELECT * FROM tareas').all();
  res.json(filas);
});

app.post('/tareas', (req, res) => {
  const { titulo } = req.body;
  if (!titulo || typeof titulo !== 'string') {
    return res.status(400).json({ error: 'El campo titulo es obligatorio' });
  }
  const info = db.prepare('INSERT INTO tareas (titulo) VALUES (?)').run(titulo);
  const nueva = db.prepare('SELECT * FROM tareas WHERE id = ?').get(info.lastInsertRowid);
  res.status(201).json(nueva);
});

El signo ? es un parámetro: la librería inserta el valor de forma segura y evita inyecciones SQL. Con la misma lógica adaptás GET /tareas/:id (con SELECT ... WHERE id = ?), PUT (con UPDATE) y DELETE; en los dos últimos, la propiedad changes del resultado te dice si existía la tarea para responder 404 si vale cero. Reiniciá el servidor y comprobá que las tareas siguen ahí.

Cuando el proyecto crezca, podés pasar a una base como PostgreSQL y usar un ORM como Prisma, que te deja trabajar con bases SQL desde JavaScript. Otras mejoras que suman mucho: separar las rutas en archivos, usar variables de entorno para el puerto y agregar un middleware de manejo de errores.

Cómo sumarlo a tu portfolio

Subí el proyecto a GitHub con un README que explique qué hace la API, cómo instalarla, la lista de endpoints con ejemplos de petición y respuesta, y qué aprendiste. Con la base de datos conectada, si además lo publicás en un servicio gratuito de hosting, tenés un proyecto que podés mostrar en una entrevista. Más ideas en proyectos backend con Node.js para tu portafolio.

Curso recomendado de Coderhouse

Si querés pasar de este primer ejemplo a construir servidores completos, la carrera de Desarrollo Backend te enseña Node.js, bases de datos SQL y NoSQL, autenticación y buenas prácticas para trabajar en proyectos reales.

Preguntas frecuentes

¿Necesito saber JavaScript antes de hacer este tutorial? Sí, al menos lo básico: variables, funciones, arreglos y objetos. Con eso podés seguir todos los pasos.

¿Express sigue siendo una buena opción para aprender? Sí. Es simple, muy documentado y muchísimas empresas lo usan. Lo que aprendas te sirve después para otros frameworks.

¿Qué diferencia hay entre PUT y PATCH? En teoría, PUT reemplaza el recurso completo y PATCH modifica solo algunos campos. Muchas APIs usan PUT para ambos casos, como en este ejemplo.

¿Cómo protejo mi API para que no cualquiera pueda borrar datos? Con autenticación, por ejemplo usando tokens. Es un buen segundo proyecto una vez que domines lo básico.

Sobre el autor

Dan Patiño

Soy Dan Patiño, responsable de AI Strategy & Innovation en Coderhouse. Mi día a día consiste en fusionar la gestión táctica del e-commerce (CRO, Email Marketing y SEO) con el desarrollo de soluciones disruptivas. Me especializo en crear apps internas con IA para automatizar tareas y potenciar la innovación dentro del equipo. Creo fielmente que la tecnología es el mejor aliado de la estrategia. Para profundizar en mi recorrido profesional, te espero en mi perfil de LinkedIn.

Imagen promocionando quiz gratis de Coderhouse para encontrar tu formación.
Imagen promocionando quiz gratis de Coderhouse para encontrar tu formación.
Imagen promocionando quiz gratis de Coderhouse para encontrar tu formación.
Imagen promocionando quiz gratis de Coderhouse para encontrar tu formación.

NEWSLETTER

Suscríbete y mantente al día con las últimas noticias, ofertas exclusivas y recursos útiles directamente en tu correo.

Global

© 2026 Coderhouse. Todos los derechos reservados.

NEWSLETTER

Suscríbete y mantente al día con las últimas noticias, ofertas exclusivas y recursos útiles directamente en tu correo.

Global

© 2026 Coderhouse. Todos los derechos reservados.

NEWSLETTER

Suscríbete y mantente al día con las últimas noticias, ofertas exclusivas y recursos útiles directamente en tu correo.

Global

© 2026 Coderhouse. Todos los derechos reservados.

NEWSLETTER

Suscríbete y mantente al día con las últimas noticias, ofertas exclusivas y recursos útiles directamente en tu correo.

Global

© 2026 Coderhouse. Todos los derechos reservados.