Home / Lenguajes / encodeURI vs encodeURIComponent: cómo codificar URLs en JavaScript
encodeURI vs encodeURIComponent: cómo codificar URLs en JavaScript

encodeURI vs encodeURIComponent: cómo codificar URLs en JavaScript

18 October 2026 · 19 min de lectura ··

Por qué codificar URLs sigue siendo un problema cotidiano

Construir una URL parece una tarea trivial: se concatena un dominio, una ruta y unos parámetros, y listo. Sin embargo, basta con que un usuario escriba un espacio, un ampersand o una tilde en un buscador para que la petición llegue rota al servidor, devuelva resultados incorrectos o, en el peor de los casos, abra la puerta a una vulnerabilidad. Codificar correctamente las URLs es una de esas habilidades básicas que separan el código que funciona en la demo del código que funciona en producción.

JavaScript ofrece desde hace muchos años dos funciones globales para esta tarea, encodeURI y encodeURIComponent. Sus nombres son tan parecidos que es fácil pensar que hacen lo mismo, y esa confusión es la causa de una cantidad sorprendente de errores. Además, el lenguaje y la plataforma web han incorporado con el tiempo herramientas de más alto nivel, como las clases URL y URLSearchParams, que hoy son la forma recomendada de manipular direcciones.

En este artículo vamos a entender qué es la codificación por porcentaje, qué caracteres transforma cada función, cuándo conviene usar cada una y cómo las APIs modernas simplifican el trabajo tanto en el navegador como en Node.js. También revisaremos los errores más frecuentes, cómo depurarlos y qué implicaciones de seguridad tiene una codificación descuidada.

URI y URL: aclarando los términos

Antes de entrar en el código conviene aclarar dos siglas que aparecen en los nombres de las funciones.

Un URI, o identificador uniforme de recursos, es una cadena que identifica un recurso de forma única. Un URL, o localizador uniforme de recursos, es un tipo particular de URI que, además de identificar el recurso, indica cómo acceder a él, normalmente mediante un protocolo y una ubicación en la red. Todo URL es un URI, pero no todo URI es un URL. Por ejemplo, urn:isbn:9788420412146 identifica un libro sin decir dónde encontrarlo, mientras que https://api.tienda.dev/libros?id=42 indica protocolo, servidor y ruta.

En la práctica diaria del desarrollo web, cuando hablamos de URIs casi siempre estamos tratando con URLs. Lo que sí importa es conocer las partes en que se divide una dirección, porque cada una admite caracteres distintos:

https://ana:[email protected]:8443/catalogo/libros/rayuela?autor=Cortázar&ed=2&orden=desc#reseñas
├── protocolo: https:
├── credenciales: ana:secreto
├── host: tienda.dev
├── puerto: 8443
├── ruta: /catalogo/libros/rayuela
├── consulta: ?autor=Cortázar&ed=2&orden=desc
└── fragmento: #reseñas

Algunos caracteres tienen un significado estructural. La barra separa segmentos de la ruta, el signo de interrogación marca el inicio de la consulta, el ampersand separa parámetros, el signo igual une clave y valor, y la almohadilla introduce el fragmento. Si uno de esos caracteres aparece dentro de un dato, por ejemplo en el título de un libro, el navegador o el servidor lo interpretarán como un separador y no como parte del valor.

Qué es la codificación por porcentaje

El estándar que define la sintaxis de los URIs solo permite un conjunto limitado de caracteres ASCII. Todo lo demás, incluidos los espacios, las letras acentuadas, los emojis y los caracteres reservados que aparecen fuera de su función estructural, debe transformarse antes de formar parte de la dirección.

La técnica que se utiliza es la codificación por porcentaje en UTF-8, definida en el estándar de los URIs. El proceso consiste en convertir el carácter a su representación en bytes UTF-8 y escribir cada byte como un signo de porcentaje seguido de dos dígitos hexadecimales. Un espacio, que en UTF-8 es el byte 0x20, se convierte en %20. La letra ñ ocupa dos bytes y se convierte en %C3%B1. Un emoji puede ocupar cuatro bytes y generar cuatro secuencias.

Podemos ver ese proceso paso a paso con un poco de código:

function mostrarBytes(caracter) {
    const bytes = new TextEncoder().encode(caracter);
    const porcentaje = [...bytes]
        .map((b) => "%" + b.toString(16).toUpperCase().padStart(2, "0"))
        .join("");
    console.log(`${caracter} → ${bytes.length} byte(s) → ${porcentaje}`);
}

[" ", "ñ", "€", "🚀"].forEach(mostrarBytes);
  → 1 byte(s) → %20
ñ → 2 byte(s) → %C3%B1
€ → 3 byte(s) → %E2%82%AC
🚀 → 4 byte(s) → %F0%9F%9A%80

Este es exactamente el trabajo que realizan las funciones de codificación de JavaScript. La diferencia entre ellas no está en cómo codifican, sino en qué caracteres deciden dejar intactos.

encodeURI: pensada para direcciones completas

La función encodeURI recibe una URL completa y la prepara para ser utilizada, respetando su estructura. Para ello deja sin modificar todos los caracteres que tienen un significado sintáctico dentro de un URI, de modo que la dirección siga funcionando después de la codificación.

En concreto, encodeURI no codifica:

  • Letras de la A a la Z en mayúsculas y minúsculas, y dígitos del 0 al 9.
  • Los caracteres no reservados -, _, ., !, ~, *, ', ( y ).
  • Los caracteres reservados ;, /, ?, :, @, &, =, +, $, , y #.

Todo lo demás, como espacios, letras acentuadas, comillas dobles o caracteres no latinos, se convierte a su forma de porcentaje:

const direccion =
    "https://tienda.dev/catálogo/ficha técnica.pdf?idioma=español#índice";

console.log(encodeURI(direccion));
https://tienda.dev/cat%C3%A1logo/ficha%20t%C3%A9cnica.pdf?idioma=espa%C3%B1ol#%C3%ADndice

El resultado sigue siendo una dirección válida y navegable. Los dos puntos del protocolo, las barras de la ruta, el signo de interrogación y la almohadilla permanecen en su sitio, mientras que los espacios y los caracteres acentuados se han transformado.

Precisamente por respetar los separadores, encodeURI no sirve para codificar datos que vayan a insertarse como valores. Si un parámetro contiene un ampersand, encodeURI lo dejará intacto y el servidor lo interpretará como el inicio de un nuevo parámetro:

const termino = "café & té";
const url = encodeURI(`https://tienda.dev/buscar?q=${termino}&pagina=1`);

console.log(url);
console.log(new URL(url).searchParams.get("q"));
https://tienda.dev/buscar?q=caf%C3%A9%20&%20t%C3%A9&pagina=1
café

El servidor recibirá café como búsqueda y un parámetro sin nombre con el valor té. El dato del usuario se ha partido en dos.

encodeURIComponent: pensada para fragmentos y valores

La función encodeURIComponent está diseñada para codificar una pieza de la URL, típicamente el valor de un parámetro de consulta o un segmento individual de la ruta. Su comportamiento es mucho más estricto: solo deja sin codificar las letras, los dígitos y los caracteres -, _, ., !, ~, *, ', ( y ). Todos los caracteres reservados, incluidos /, ?, &, =, #, + y :, se codifican.

Corregir el ejemplo anterior es tan sencillo como aplicar encodeURIComponent al valor, y no a la URL completa:

const termino = "café & té";
const url = `https://tienda.dev/buscar?q=${encodeURIComponent(termino)}&pagina=1`;

console.log(url);
console.log(new URL(url).searchParams.get("q"));
https://tienda.dev/buscar?q=caf%C3%A9%20%26%20t%C3%A9&pagina=1
café & té

Ahora el ampersand del dato aparece como %26 y el servidor recupera el valor original completo.

Si aplicamos encodeURIComponent a una dirección entera, el resultado deja de ser una URL utilizable, porque también se codifican los separadores:

console.log(encodeURIComponent("https://tienda.dev/buscar?q=libros"));
https%3A%2F%2Ftienda.dev%2Fbuscar%3Fq%3Dlibros

Este resultado no es un error en sí mismo. Es justo lo que necesitamos cuando una URL completa debe viajar como valor de otro parámetro, por ejemplo en una redirección después de iniciar sesión:

const destino = "https://tienda.dev/carrito?cupon=OTOÑO26";
const login = `https://cuentas.tienda.dev/entrar?volver=${encodeURIComponent(destino)}`;

console.log(login);
https://cuentas.tienda.dev/entrar?volver=https%3A%2F%2Ftienda.dev%2Fcarrito%3Fcupon%3DOTO%C3%91O26

La diferencia entre encodeURI y encodeURIComponent en una tabla

Resumir la diferencia entre encodeURI y encodeURIComponent en una sola comparación ayuda a fijar la regla. El siguiente script codifica los caracteres más conflictivos con ambas funciones:

const caracteres = [
    " ",
    "/",
    "?",
    "&",
    "=",
    "#",
    "+",
    ":",
    "@",
    ",",
    ";",
    "$",
    "ñ",
    "%",
];

console.table(
    caracteres.map((c) => ({
        caracter: c,
        encodeURI: encodeURI(c),
        encodeURIComponent: encodeURIComponent(c),
    }))
);
┌─────────┬──────────┬───────────┬────────────────────┐
│ (index) │ caracter │ encodeURI │ encodeURIComponent │
├─────────┼──────────┼───────────┼────────────────────┤
│ 0       │ ' '      │ '%20'     │ '%20'              │
│ 1       │ '/'      │ '/'       │ '%2F'              │
│ 2       │ '?'      │ '?'       │ '%3F'              │
│ 3       │ '&'      │ '&'       │ '%26'              │
│ 4       │ '='      │ '='       │ '%3D'              │
│ 5       │ '#'      │ '#'       │ '%23'              │
│ 6       │ '+'      │ '+'       │ '%2B'              │
│ 7       │ ':'      │ ':'       │ '%3A'              │
│ 8       │ '@'      │ '@'       │ '%40'              │
│ 9       │ ','      │ ','       │ '%2C'              │
│ 10      │ ';'      │ ';'       │ '%3B'              │
│ 11      │ '$'      │ '$'       │ '%24'              │
│ 12      │ 'ñ'      │ '%C3%B1'  │ '%C3%B1'           │
│ 13      │ '%'      │ '%25'     │ '%25'              │
└─────────┴──────────┴───────────┴────────────────────┘

La regla práctica que se desprende es clara. Si tienes una dirección completa y solo necesitas sanear caracteres como espacios o acentos, usa encodeURI. Si tienes un dato que va a formar parte de la dirección, usa encodeURIComponent. En la inmensa mayoría de los casos reales, lo que se necesita es la segunda.

Observa también que ambas funciones codifican el signo de porcentaje como %25. Esto tiene una consecuencia importante: si codificas dos veces la misma cadena, el resultado será diferente al original. Volveremos sobre ello en la sección de errores.

Decodificar con decodeURI y decodeURIComponent

Cada función de codificación tiene su pareja inversa. decodeURI revierte el trabajo de encodeURI, y decodeURIComponent revierte el de encodeURIComponent:

const codificado = "caf%C3%A9%20%26%20t%C3%A9";

console.log(decodeURIComponent(codificado));
console.log(decodeURI(codificado));
café & té
café %26 té

El segundo resultado ilustra un detalle sutil. Como encodeURI nunca codifica el ampersand, decodeURI tampoco decodifica %26, para no alterar la estructura de la dirección. Por eso conviene usar siempre la función de decodificación que corresponde a la de codificación.

Las funciones de decodificación lanzan una excepción cuando reciben una secuencia que no es válida en UTF-8 o un porcentaje que no va seguido de dos dígitos hexadecimales. Es un caso muy habitual cuando los datos provienen de usuarios o de sistemas externos:

function decodificarSeguro(valor) {
    try {
        return decodeURIComponent(valor);
    } catch (error) {
        if (error instanceof URIError) return valor;
        throw error;
    }
}

console.log(decodificarSeguro("descuento%2050%"));
console.log(decodificarSeguro("100%25%20natural"));
descuento%2050%
100% natural

Envolver la decodificación en un bloque try evita que una sola URL malformada tumbe un proceso completo, algo especialmente relevante en servidores.

Las APIs URL y URLSearchParams

Las funciones globales son útiles, pero obligan a construir las direcciones concatenando cadenas, lo que resulta propenso a errores. La plataforma web ofrece desde hace años dos clases que resuelven el problema de raíz: URL y URLSearchParams. Están disponibles en todos los navegadores modernos y en Node.js como objetos globales, sin necesidad de importar nada, y en este momento Node.js 24 es la rama de soporte activo mientras Node.js 26 se prepara para incorporarse al ciclo LTS.

La clase URL analiza una dirección y expone cada una de sus partes como propiedades:

const url = new URL(
    "https://tienda.dev:8443/catalogo/libros?orden=desc#reseñas"
);

console.log(url.protocol); // 'https:'
console.log(url.hostname); // 'tienda.dev'
console.log(url.port); // '8443'
console.log(url.pathname); // '/catalogo/libros'
console.log(url.search); // '?orden=desc'
console.log(url.hash); // '#rese%C3%B1as'

La propiedad searchParams devuelve un objeto URLSearchParams que permite añadir, leer y eliminar parámetros sin preocuparse por la codificación:

const busqueda = new URL("https://tienda.dev/buscar");

busqueda.searchParams.set("q", "café & té");
busqueda.searchParams.set("categoria", "infusiones/orgánicas");
busqueda.searchParams.append("etiqueta", "otoño");
busqueda.searchParams.append("etiqueta", "oferta");

console.log(busqueda.href);
console.log(busqueda.searchParams.getAll("etiqueta"));
https://tienda.dev/buscar?q=caf%C3%A9+%26+t%C3%A9&categoria=infusiones%2Forg%C3%A1nicas&etiqueta=oto%C3%B1o&etiqueta=oferta
[ 'otoño', 'oferta' ]

Trabajar con parámetros de consulta en URL de esta manera elimina toda una categoría de errores: no hay que recordar qué función aplicar, no hay riesgo de codificar dos veces y los parámetros repetidos se gestionan de forma natural.

También se puede crear un URLSearchParams a partir de un objeto, lo que resulta muy cómodo para construir peticiones a APIs:

const filtros = { autor: "García Márquez", desde: 1960, formato: "tapa dura" };
const query = new URLSearchParams(filtros);

const respuesta = await fetch(`https://api.tienda.dev/libros?${query}`);

Para validar direcciones sin necesidad de capturar excepciones existe el método estático URL.canParse, y para obtener directamente un objeto o null está URL.parse, ambos disponibles en las versiones actuales de los navegadores y de Node.js:

console.log(URL.canParse("https://tienda.dev")); // true
console.log(URL.canParse("tienda.dev")); // false: falta el protocolo
console.log(URL.parse("no es una url")); // null

El constructor de URL también admite una dirección base, lo que facilita resolver rutas relativas:

const base = "https://tienda.dev/catalogo/libros/";
console.log(new URL("../revistas/marzo", base).href);
https://tienda.dev/catalogo/revistas/marzo

Espacios: %20 frente al signo más

Si comparas la salida de encodeURIComponent con la de URLSearchParams, verás una diferencia: la primera codifica el espacio como %20 y la segunda como +. Ambas son correctas, pero responden a especificaciones distintas.

El signo más como espacio proviene del formato application/x-www-form-urlencoded, el que usan los formularios HTML al enviarse. URLSearchParams sigue ese formato porque su propósito es representar precisamente esos datos. En cambio, encodeURIComponent sigue las reglas generales de los URIs, donde el espacio siempre es %20.

const texto = "ciencia ficción";

console.log(encodeURIComponent(texto));
console.log(new URLSearchParams({ q: texto }).toString());
ciencia%20ficci%C3%B3n
q=ciencia+ficci%C3%B3n

En la parte de consulta de una URL, prácticamente todos los servidores entienden ambas formas. El problema aparece al mezclar herramientas: si decodificas una consulta generada por un formulario con decodeURIComponent, los signos más permanecerán como tales:

console.log(decodeURIComponent("ciencia+ficci%C3%B3n"));
console.log(new URLSearchParams("q=ciencia+ficci%C3%B3n").get("q"));
ciencia+ficción
ciencia ficción

La recomendación es leer siempre las cadenas de consulta con URLSearchParams, que interpreta correctamente ambos formatos. En la ruta, en cambio, el signo más no significa espacio, por lo que los segmentos deben codificarse con encodeURIComponent.

Codificar segmentos de ruta dinámicos

Las APIs REST suelen incluir identificadores o nombres dentro de la ruta, como /usuarios/{nombre}/archivos/{archivo}. Si uno de esos valores contiene una barra, un signo de interrogación o una almohadilla, cambiará el significado de la dirección. Cada segmento debe codificarse por separado:

function construirRuta(plantilla, valores) {
    return plantilla.replace(/\{(\w+)\}/g, (_, clave) => {
        if (!(clave in valores))
            throw new Error(`Falta el valor para ${clave}`);
        return encodeURIComponent(String(valores[clave]));
    });
}

const ruta = construirRuta("/usuarios/{usuario}/archivos/{archivo}", {
    usuario: "lucía.p",
    archivo: "informe Q3/2026 #final.pdf",
});

console.log(new URL(ruta, "https://api.tienda.dev").href);
https://api.tienda.dev/usuarios/luc%C3%ADa.p/archivos/informe%20Q3%2F2026%20%23final.pdf

Una alternativa muy elegante en JavaScript moderno son las plantillas etiquetadas, que codifican automáticamente cada valor interpolado y dejan intacto el texto literal:

function url(cadenas, ...valores) {
    return cadenas.reduce(
        (acc, texto, i) =>
            acc +
            texto +
            (i < valores.length ? encodeURIComponent(valores[i]) : ""),
        ""
    );
}

const usuario = "ana/admin";
const pedido = "A#77";
console.log(url`/usuarios/${usuario}/pedidos/${pedido}`);
/usuarios/ana%2Fadmin/pedidos/A%2377

Codificación en el servidor con Node.js

Las mismas funciones y clases están disponibles en Node.js, lo que permite compartir la lógica entre cliente y servidor. Un servidor HTTP mínimo puede analizar las peticiones entrantes con URL:

// servidor.mjs
import { createServer } from "node:http";

const servidor = createServer((req, res) => {
    const url = new URL(req.url, `http://${req.headers.host}`);

    if (url.pathname === "/buscar") {
        const termino = url.searchParams.get("q") ?? "";
        const pagina = Number(url.searchParams.get("pagina") ?? 1);

        res.writeHead(200, {
            "Content-Type": "application/json; charset=utf-8",
        });
        res.end(JSON.stringify({ termino, pagina }));
        return;
    }

    res.writeHead(404).end();
});

servidor.listen(3000, () => console.log("Escuchando en http://localhost:3000"));
node servidor.mjs
curl "http://localhost:3000/buscar?q=caf%C3%A9%20%26%20t%C3%A9&pagina=2"
{"termino":"café & té","pagina":2}

Node.js conserva también el antiguo módulo querystring, pero la documentación oficial lo considera heredado y recomienda URLSearchParams en su lugar. Del mismo modo, la función url.parse() del módulo node:url está desaconsejada por sus inconsistencias y posibles problemas de seguridad; la clase URL es la alternativa correcta.

Los frameworks web más populares, como Express, Fastify o Hono, decodifican automáticamente los parámetros de ruta y de consulta antes de entregarlos a tus controladores. Por eso, dentro de un controlador no debes volver a llamar a decodeURIComponent sobre esos valores, o corromperás los datos que contengan un signo de porcentaje.

Errores comunes y cómo resolverlos

La mayoría de los problemas de codificación se repiten en todos los proyectos. Conocerlos de antemano ahorra horas de depuración.

El error más habitual es usar encodeURI para codificar valores. Como vimos, deja intactos &, =, # y +, de modo que cualquier dato que los contenga partirá los parámetros. La solución es usar encodeURIComponent para los valores o, mejor aún, URLSearchParams.

El segundo es la doble codificación. Ocurre cuando un valor ya codificado se vuelve a codificar, normalmente porque una función intermedia y el cliente HTTP aplican la codificación por separado:

const valor = "precio 50%";
const una = encodeURIComponent(valor);
const dos = encodeURIComponent(una);

console.log(una);
console.log(dos);
precio%2050%25
precio%252050%2525

El síntoma típico es ver secuencias como %2520 o %25C3 en los registros del servidor o en los datos almacenados. La solución es establecer un único punto de la aplicación responsable de codificar y trabajar siempre con los valores originales hasta ese punto.

El tercero es decodificar datos que no estaban codificados. Si un texto contiene un porcentaje literal, decodeURIComponent lanzará una excepción como esta:

Uncaught URIError: URI malformed
    at decodeURIComponent (<anonymous>)

Se resuelve decodificando solo los datos que sabemos que están codificados y protegiendo la llamada con try y catch cuando el origen no es fiable.

El cuarto es olvidar el protocolo al crear un objeto URL. El constructor exige una dirección absoluta o una base:

TypeError: Invalid URL
    code: 'ERR_INVALID_URL',
    input: 'tienda.dev/buscar'

Basta con añadir el esquema o pasar una URL base como segundo argumento.

El quinto aparece con caracteres sustitutos de UTF-16 aislados, que pueden surgir al cortar una cadena por la mitad de un emoji. encodeURIComponent no puede representarlos y lanza un URIError. Si trabajas con texto truncado, el método toWellFormed de las cadenas, disponible en los entornos actuales, reemplaza esos fragmentos por un carácter de sustitución válido:

const recortado = "¡Oferta! 🎁".slice(0, -1); // corta el emoji a la mitad
console.log(encodeURIComponent(recortado.toWellFormed()));

Casos de uso reales

La teoría se entiende mejor con situaciones que aparecen a diario en cualquier proyecto web. Estos son algunos escenarios donde una codificación correcta marca la diferencia.

Un buscador con filtros es el ejemplo más evidente. El usuario escribe un término libre, elige una categoría y un rango de precios, y la aplicación debe reflejarlo en la barra de direcciones para que la búsqueda se pueda compartir o guardar en favoritos. Con URLSearchParams y la API de historial del navegador, el estado de la interfaz y la URL se mantienen sincronizados sin esfuerzo:

function actualizarFiltros(filtros) {
    const url = new URL(window.location.href);
    for (const [clave, valor] of Object.entries(filtros)) {
        if (valor === "" || valor == null) url.searchParams.delete(clave);
        else url.searchParams.set(clave, valor);
    }
    history.replaceState(null, "", url);
}

actualizarFiltros({ q: "novela negra", precioMax: 25, idioma: "español" });

Los enlaces para compartir en redes sociales o por correo son otro caso habitual. Un enlace mailto necesita que el asunto y el cuerpo estén codificados para que los saltos de línea, los acentos y los signos de puntuación lleguen intactos al cliente de correo. Aquí conviene encodeURIComponent, porque muchos clientes de correo no interpretan el signo más como espacio:

const asunto = "Pedido #4521: consulta sobre envío";
const cuerpo = "Hola,\n¿podrían confirmar la fecha de entrega?\nGracias.";
const enlace = `mailto:[email protected]?subject=${encodeURIComponent(asunto)}&body=${encodeURIComponent(cuerpo)}`;

Las integraciones con APIs de terceros también dependen de una codificación precisa. Algunos servicios firman las peticiones calculando un hash sobre la URL exacta, de modo que una diferencia entre %20 y +, o entre mayúsculas y minúsculas en las secuencias hexadecimales, invalida la firma. En esos casos hay que seguir al pie de la letra las reglas de codificación que documenta el proveedor y, a menudo, construir la cadena canónica manualmente con encodeURIComponent.

Por último, los flujos de autenticación con OAuth y OpenID Connect transportan direcciones completas como valores de parámetros, como la URL de retorno o el estado de la sesión. Ese es justo el escenario en el que una URL debe codificarse como componente para viajar dentro de otra, y donde validar el destino evita redirecciones abiertas.

Seguridad: la codificación no es sanitización

Codificar una URL correctamente evita que los datos alteren su estructura, y eso ya es una medida de seguridad importante. Un atacante que controla un parámetro sin codificar podría inyectar otros parámetros, por ejemplo añadiendo &rol=admin a una petición interna. Esta técnica se conoce como contaminación de parámetros y la codificación sistemática la neutraliza.

Sin embargo, conviene no confundir codificación con validación. Codificar no hace que un valor sea seguro para cualquier contexto.

La primera precaución tiene que ver con los esquemas peligrosos. Si una aplicación toma una dirección proporcionada por el usuario y la coloca en un enlace, un valor como javascript:alert(document.cookie) sigue siendo ejecutable aunque esté codificado. Antes de usar una URL externa hay que validar su protocolo:

function esEnlaceSeguro(entrada) {
    const url = URL.parse(entrada);
    return url !== null && ["https:", "http:"].includes(url.protocol);
}

console.log(esEnlaceSeguro("https://tienda.dev/ofertas")); // true
console.log(esEnlaceSeguro("javascript:alert(1)")); // false

La segunda son las redirecciones abiertas. Un parámetro como volver, que indica a dónde enviar al usuario tras iniciar sesión, puede usarse para redirigir a un sitio fraudulento. Hay que comprobar que el destino pertenece al propio dominio:

function destinoPermitido(valor, origenPropio = "https://tienda.dev") {
    const url = URL.parse(valor, origenPropio);
    return url?.origin === origenPropio ? url.href : origenPropio;
}

La tercera es la inserción en HTML. Una URL codificada para la red no está escapada para HTML. Si la insertas en el documento, usa las APIs del DOM, como asignar href a un elemento o textContent, en lugar de concatenar cadenas en innerHTML.

Por último, evita incluir datos sensibles en la consulta de una URL. Contraseñas, tokens o datos personales quedan registrados en el historial del navegador, en los registros de los servidores y en los encabezados de referencia. Para esos datos, utiliza el cuerpo de una petición POST o los encabezados de autorización.

Buenas prácticas para el día a día

Después de revisar todas las herramientas, estas son las pautas que funcionan bien en proyectos reales.

Usa URL y URLSearchParams como opción por defecto. Construir direcciones mediante objetos en lugar de concatenar cadenas elimina la mayoría de los errores de codificación y hace el código más legible.

Reserva encodeURIComponent para los casos que esas APIs no cubren directamente, como codificar segmentos de ruta dinámicos o generar cadenas para sistemas que exigen %20 en lugar del signo más.

Limita encodeURI a situaciones concretas en las que recibes una dirección completa con espacios o caracteres no ASCII y necesitas normalizarla sin alterar su estructura. En código nuevo, new URL(direccion).href suele cumplir mejor ese papel.

Codifica en un único lugar. Define una capa, como un cliente de API o una función auxiliar, que sea la única responsable de construir direcciones. Así evitarás la doble codificación.

Valida además de codificar. Comprueba protocolos, dominios y formatos de los valores que provienen del exterior antes de usarlos.

Escribe pruebas con datos difíciles. Incluir espacios, ampersands, signos de porcentaje, barras, acentos y emojis en los casos de prueba destapa casi todos los problemas:

import { test } from "node:test";
import assert from "node:assert/strict";

const casos = ["café & té", "50% dto.", "a/b?c#d", "año 🚀", "  espacios  "];

test("los parámetros sobreviven al viaje de ida y vuelta", () => {
    for (const valor of casos) {
        const url = new URL("https://tienda.dev/buscar");
        url.searchParams.set("q", valor);
        assert.equal(new URL(url.href).searchParams.get("q"), valor);
    }
});
node --test

Conclusiones

Codificar URLs en JavaScript deja de ser confuso en cuanto se entiende el criterio que separa a las dos funciones clásicas. encodeURI está pensada para direcciones completas y respeta los caracteres que dan estructura a la URL, mientras que encodeURIComponent está pensada para los datos que se insertan en ella y codifica todo lo que podría interpretarse como un separador. Elegir la equivocada es la causa más frecuente de parámetros partidos y búsquedas que devuelven resultados extraños.

Cada función tiene su pareja de decodificación, y ambas lanzan excepciones ante datos malformados, por lo que conviene protegerlas cuando el origen no es fiable. Los problemas de doble codificación y la diferencia entre %20 y el signo más explican la mayoría de los comportamientos inesperados que aparecen en los registros de un servidor.

Para el código nuevo, las clases URL y URLSearchParams son la herramienta recomendada tanto en el navegador como en Node.js. Analizan, construyen y codifican direcciones de forma consistente, gestionan los parámetros repetidos y ofrecen métodos como URL.canParse y URL.parse para validar sin capturar excepciones.

Por último, la codificación es una pieza de la seguridad, pero no la única. Validar protocolos, controlar las redirecciones y no exponer datos sensibles en la consulta completan una estrategia sólida para manejar URLs en cualquier aplicación web.