Tema
Servidor HTTP nativo
Antes de usar Express, vale la pena construir un servidor sin frameworks con el módulo http. Entenderás qué es una petición, una respuesta, los headers y los códigos de estado. Todo lo que hace Express por debajo.
Concepto
La web funciona con el protocolo HTTP: un cliente (navegador, app, Postman) envía una petición (request) y el servidor responde con una respuesta (response).
txt
Cliente Servidor
│ GET /productos HTTP/1.1 │
│ Host: localhost:3000 │
│ Accept: application/json ───────────► │
│ │ procesa
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ ◄─────────── [{"id":1,...}] │Una petición tiene:
- Método: qué quieres hacer (
GETleer,POSTcrear,PUT/PATCHactualizar,DELETEborrar). - URL: ruta y parámetros (
/productos?categoria=ropa). - Headers: metadatos (tipo de contenido, autenticación, idioma).
- Body: datos enviados (en POST, PUT, PATCH).
Una respuesta tiene:
- Código de estado:
200OK,201creado,404no encontrado,500error del servidor. - Headers:
Content-Type, cookies, caché. - Body: HTML, JSON, una imagen...
Ejemplo: el servidor más simple
js
// servidor.js
import http from 'node:http'
const servidor = http.createServer((req, res) => {
// req: la petición (Readable stream)
// res: la respuesta (Writable stream)
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end('¡Hola desde Node! 👋')
})
const PUERTO = 3000
servidor.listen(PUERTO, () => {
console.log(`Servidor en http://localhost:${PUERTO}`)
})bash
node --watch servidor.jsAbre http://localhost:3000 en el navegador o usa curl:
bash
curl -i http://localhost:3000Rutas y métodos
js
// rutas.js
import http from 'node:http'
const servidor = http.createServer((req, res) => {
// new URL separa ruta y parámetros de consulta
const url = new URL(req.url, `http://${req.headers.host}`)
const { pathname, searchParams } = url
console.log(`${req.method} ${pathname}`)
if (req.method === 'GET' && pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
return res.end('<h1>Inicio</h1><a href="/saludo?nombre=Ana">Saludar</a>')
}
if (req.method === 'GET' && pathname === '/saludo') {
const nombre = searchParams.get('nombre') ?? 'visitante'
res.writeHead(200, { 'Content-Type': 'application/json' })
return res.end(JSON.stringify({ mensaje: `Hola, ${nombre}` }))
}
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Ruta no encontrada' }))
})
servidor.listen(3000, () => console.log('http://localhost:3000'))Leer el body de una petición
El body llega como stream, en chunks. Hay que juntarlos:
js
// Función auxiliar para leer JSON del body
async function leerJson(req) {
const chunks = []
for await (const chunk of req) {
chunks.push(chunk)
}
const texto = Buffer.concat(chunks).toString('utf8')
return texto ? JSON.parse(texto) : {}
}Caso real: mini API REST sin frameworks
js
// api.js
import http from 'node:http'
// "Base de datos" en memoria
let productos = [
{ id: 1, nombre: 'Teclado', precio: 899 },
{ id: 2, nombre: 'Mouse', precio: 349 }
]
let siguienteId = 3
function enviarJson(res, status, datos) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
res.end(JSON.stringify(datos))
}
async function leerJson(req) {
const chunks = []
let tamaño = 0
for await (const chunk of req) {
tamaño += chunk.length
if (tamaño > 1e6) throw new Error('Body demasiado grande') // límite 1 MB
chunks.push(chunk)
}
const texto = Buffer.concat(chunks).toString('utf8')
return texto ? JSON.parse(texto) : {}
}
const servidor = http.createServer(async (req, res) => {
const { pathname } = new URL(req.url, `http://${req.headers.host}`)
// /productos/2 → ['', 'productos', '2']
const [, recurso, idTexto] = pathname.split('/')
const id = Number(idTexto)
try {
if (recurso !== 'productos') {
return enviarJson(res, 404, { error: 'Ruta no encontrada' })
}
// GET /productos
if (req.method === 'GET' && !idTexto) {
return enviarJson(res, 200, productos)
}
// GET /productos/:id
if (req.method === 'GET' && idTexto) {
const producto = productos.find((p) => p.id === id)
if (!producto) return enviarJson(res, 404, { error: 'Producto no encontrado' })
return enviarJson(res, 200, producto)
}
// POST /productos
if (req.method === 'POST' && !idTexto) {
const { nombre, precio } = await leerJson(req)
if (typeof nombre !== 'string' || typeof precio !== 'number' || precio < 0) {
return enviarJson(res, 400, { error: 'nombre (texto) y precio (número >= 0) son obligatorios' })
}
const nuevo = { id: siguienteId++, nombre, precio }
productos.push(nuevo)
return enviarJson(res, 201, nuevo)
}
// DELETE /productos/:id
if (req.method === 'DELETE' && idTexto) {
const antes = productos.length
productos = productos.filter((p) => p.id !== id)
if (productos.length === antes) return enviarJson(res, 404, { error: 'Producto no encontrado' })
res.writeHead(204) // sin contenido
return res.end()
}
enviarJson(res, 405, { error: 'Método no permitido' })
} catch (error) {
if (error instanceof SyntaxError) {
return enviarJson(res, 400, { error: 'JSON inválido' })
}
console.error(error)
enviarJson(res, 500, { error: 'Error interno del servidor' })
}
})
servidor.listen(3000, () => console.log('API en http://localhost:3000/productos'))Pruébala:
bash
curl http://localhost:3000/productos
curl http://localhost:3000/productos/1
curl -X POST http://localhost:3000/productos -H "Content-Type: application/json" -d '{"nombre":"Monitor","precio":3499}'
curl -X DELETE -i http://localhost:3000/productos/2En PowerShell
curl es un alias distinto. Usa curl.exe o Invoke-RestMethod:
powershell
Invoke-RestMethod -Method Post -Uri http://localhost:3000/productos -ContentType "application/json" -Body '{"nombre":"Monitor","precio":3499}'Servir archivos estáticos
js
// estaticos.js
import http from 'node:http'
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import path from 'node:path'
const PUBLICA = path.join(import.meta.dirname, 'public')
const TIPOS = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css',
'.js': 'text/javascript',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml'
}
http.createServer(async (req, res) => {
const { pathname } = new URL(req.url, 'http://localhost')
const relativa = pathname === '/' ? 'index.html' : decodeURIComponent(pathname)
const ruta = path.join(PUBLICA, relativa)
// Seguridad: impedir salir de la carpeta pública con ../../
if (!ruta.startsWith(PUBLICA)) {
res.writeHead(403)
return res.end('Prohibido')
}
try {
const info = await stat(ruta)
if (!info.isFile()) throw new Error()
res.writeHead(200, { 'Content-Type': TIPOS[path.extname(ruta)] ?? 'application/octet-stream' })
createReadStream(ruta).pipe(res) // stream: memoria constante
} catch {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
res.end('No encontrado')
}
}).listen(3000, () => console.log('http://localhost:3000'))Error común
Olvidar res.end(). El navegador se queda "cargando" para siempre porque la respuesta nunca termina. Toda ruta debe terminar en res.end() (o un pipe a res).
Seguridad
Nunca construyas rutas de archivo con datos del usuario sin validar. Un atacante podría pedir /../../etc/passwd (path traversal). Valida que la ruta final siga dentro de tu carpeta.
Como ves, sin framework hay que hacer todo a mano: parsear URLs, leer el body, manejar errores. Por eso en la siguiente lección usaremos Express.
Ejercicios
- Agrega a la mini API la ruta
PUT /productos/:idque actualice nombre y/o precio. - Agrega soporte para
GET /productos?max=500que filtre por precio máximo.
Ver solución
js
// Dentro del try, antes de la respuesta 405:
// PUT /productos/:id
if (req.method === 'PUT' && idTexto) {
const producto = productos.find((p) => p.id === id)
if (!producto) return enviarJson(res, 404, { error: 'Producto no encontrado' })
const { nombre, precio } = await leerJson(req)
if (nombre !== undefined && typeof nombre !== 'string') {
return enviarJson(res, 400, { error: 'nombre debe ser texto' })
}
if (precio !== undefined && (typeof precio !== 'number' || precio < 0)) {
return enviarJson(res, 400, { error: 'precio debe ser número >= 0' })
}
if (nombre !== undefined) producto.nombre = nombre
if (precio !== undefined) producto.precio = precio
return enviarJson(res, 200, producto)
}js
// Reemplaza la ruta GET /productos por:
const { pathname, searchParams } = new URL(req.url, `http://${req.headers.host}`)
// ...
if (req.method === 'GET' && !idTexto) {
const max = Number(searchParams.get('max'))
const resultado = searchParams.has('max') && !Number.isNaN(max)
? productos.filter((p) => p.precio <= max)
: productos
return enviarJson(res, 200, resultado)
}Resumen
http.createServer((req, res) => ...)crea un servidor;listen(puerto)lo arranca.- Usa
new URL(req.url, base)para separar ruta y query string. - El body llega como stream: júntalo con
for awaity conviértelo conJSON.parse. - Responde con el código correcto (
200,201,204,400,404,500) y elContent-Typeadecuado.