Desde que programo en Java, una de las excepciones más recurrentes y, por qué no decirlo, molestas, ha sido la infame NullPointerException. Durante años, la forma de evitarla era anidar bloques if por todo el código, lo que no solo lo hacía más verboso, sino también más difícil de leer y mantener. Siempre quedaba la otra opción (la vieja confiable): meter un try/catch a todo el bloque sospechoso, pero esto, claramente, no mejora la legibilidad y abre otros problemas, como saber qué hacer cuando ocurre la NullPointerException.
A partir de Java 8, se introdujo una nueva herramienta en el lenguaje para lidiar con este problema de una manera más elegante y funcional: la clase Optional<T>.
¿Desde cuándo está disponible?
Optional fue una de las adiciones más destacadas del JDK 8, lanzado en marzo de 2014. Nació como parte del esfuerzo por modernizar el lenguaje e introducir paradigmas de programación funcional, junto con las expresiones Lambda y la API de Streams.
¿Cuál es su objetivo principal?
El propósito fundamental de Optional es proporcionar un contenedor que puede o no tener un valor no nulo. En lugar de devolver null para indicar la ausencia de un resultado, un método puede devolver un Optional que esté “vacío”.
Esto tiene una ventaja semántica enorme: la firma de un método que devuelve Optional<String> comunica explícitamente al programador que el valor podría no estar presente. Obliga a quien llama al método a “desenvolver” el Optional y manejar conscientemente el caso de ausencia, eliminando la ambigüedad de un null que podría ser un valor esperado o un error no controlado.
Beneficios de usar Optional
Adoptar Optional en tu código trae varias ventajas:
Claridad en el diseño de APIs: Como mencioné, si un método devuelve Optional<T>, sabes de inmediato que debes prepararte para un resultado vacío. Se acabaron las sorpresas con valores nulos inesperados.
Reducción de NullPointerException: Al forzar un manejo explícito de la ausencia de valor, el código se vuelve más robusto y menos propenso a errores en tiempo de ejecución.
Código más expresivo y funcional: Optional viene con una serie de métodos de estilo funcional (map, flatMap, filter, ifPresent) que permiten encadenar operaciones de una manera fluida y legible, evitando la necesidad de bloques if-else anidados.
Ejemplos prácticos
Veamos cómo trabajar con Optional.
Creación de un Optional
1 2 3 4 5 6 7 8 9 10 11 12
// Ejemplos de creación de un Optional // 1. Optional con un valor no nulo // (si 'valor' es null, lanza NullPointerException) Optional<String> optionalConValor = Optional.of("Hola Mundo");
// 2. Optional que permite valores nulos // (si 'valorNulo' es null, crea un Optional vacío) StringvalorNulo=null; Optional<String> optionalVacio = Optional.ofNullable(valorNulo);
// 3. Creación explícita de un Optional vacío Optional<String> vacio = Optional.empty();
Comprobando si hay un valor
1 2 3 4 5 6 7 8 9 10
// Ejemplos de comprobación de valor if (optionalConValor.isPresent()) { System.out.println("El optional tiene un valor: " + optionalConValor.get()); }
// A partir de Java 11, puedes usar isEmpty() if (vacio.isEmpty()) { System.out.println("El optional está vacío."); }
Ejecutando acciones si hay un valor
Una forma muy común y limpia de usar Optional es con ifPresent.
1 2 3 4
// Ejemplo de uso de ifPresent con una expresión lambda optionalConValor.ifPresent(valor -> { System.out.println("El valor es: " + valor); });
Obteniendo el valor de forma segura
El método .get() es peligroso porque lanzará una NoSuchElementException si el Optional está vacío. Es mucho mejor usar alternativas que proveen un valor por defecto.
1 2 3 4 5 6 7 8 9 10 11 12 13
// Formas seguras de obtener el valor // Si el optional está vacío, devuelve "default" Stringresultado1= optionalVacio.orElse("default");
// Similar a orElse, pero el valor por defecto se genera a // través de un Supplier. Esto es más eficiente si la generación // del valor por defecto es costosa. Stringresultado2= optionalVacio.orElseGet(() -> "valor generado");
// Si el optional está vacío, lanza una excepción. Stringresultado3= optionalVacio.orElseThrow(() -> newIllegalStateException("No se encontró el valor") );
Transformando valores con map y flatMap
Aquí es donde Optional realmente brilla, permitiendo un estilo de programación funcional.
// Transformando el valor con map y flatMap Optional<String> optionalString = Optional.of(" texto con espacios ");
// Usamos map para transformar el valor interno de forma segura Optional<String> sinEspacios = optionalString.map(String::trim);
// Imprime "'texto con espacios'" sinEspacios.ifPresent(s -> System.out.println("'" + s + "'"));
// flatMap se usa cuando la función map devuelve otro Optional, // para evitar tener un Optional<Optional<T>>. Optional<Optional<String>> anidado = sinEspacios.map(s -> Optional.of(s.toUpperCase()) // ¡No hagas esto! );
Optional<String> enMayusculas = sinEspacios.flatMap(s -> Optional.of(s.toUpperCase()) // ¡Esta es la forma correcta! );
// Imprime "TEXTO CON ESPACIOS" enMayusculas.ifPresent(System.out::println);
Sin lugar a dudas, lo que más uso de Optional es en conjunto con Spring Data, específicamente en los repositorios. Funcionalmente, el patrón es algo como esto: obtengo la entidad que busco y, si no existe, lanzo una excepción ExampleNotFoundException (que hereda de RuntimeException) que luego es capturada por un GlobalExceptionHandler para devolver un código de estado HTTP 404. Te invito a revisar mi artículo sobre Manejo de excepciones en Spring Boot.
1 2 3 4 5
// Ejemplo con un repositorio de Spring Data public ExampleModel findById(String id) { returnthis.exampleRepository.findById(id) .orElseThrow(() -> newExampleNotFoundException(id)); }
Palabras al cierre
Optional no es una solución mágica para todos los problemas de null, pero es una herramienta poderosa para diseñar APIs más claras y escribir código más seguro y expresivo. Al principio puede costar acostumbrarse a su enfoque funcional, pero una vez que lo dominas, te preguntarás cómo pudiste vivir sin él.
Para seguir leyendo
Si quieres profundizar más en el uso y las buenas prácticas de Optional, te recomiendo los siguientes recursos:
A Guide to Java 8 Optional (en inglés): Baeldung es un referente en tutoriales de Java. Este artículo es una guía muy completa y con excelentes ejemplos.
Java Optional y NullPointerException: Un buen artículo en español que explica el propósito de Optional y cómo ayuda a evitar los NullPointerException.
BDD (Behavior-Driven Development) es una metodología de desarrollo que coloca el comportamiento del software en el centro del proceso. A diferencia del testing unitario tradicional que se enfoca en “¿qué hace esta función?”, BDD se pregunta “¿cuál es el comportamiento que espera el usuario o el negocio?”.
En BDD, los tests se escriben usando una estructura clara y legible:
Given (Dado): El contexto o estado inicial.
When (Cuando): La acción que realizamos.
Then (Entonces): El resultado esperado.
Esta estructura hace que los tests sean casi documentación viva del sistema. No solo validan que el código funciona, sino que también comunican por qué y cómo debe comportarse.
Fortalezas de BDD
Claridad: Los tests se leen como especificaciones. Cualquiera (técnico o no) puede entender qué se está probando.
Documentación actualizada: El comportamiento del código siempre está documentado en los tests; no se queda obsoleto como un README abandonado.
Colaboración efectiva: Diseñadores, QA y desarrolladores pueden discutir el comportamiento esperado usando el mismo lenguaje.
Menos bugs por ambigüedad: Al escribir primero el comportamiento esperado, se reducen las interpretaciones erróneas.
Refactoring seguro: Con tests BDD claros, puedes modificar la implementación sin miedo a romper el comportamiento.
Por qué BDD en Deno
Deno nació con premisas fuertes en seguridad, modernidad y simplicidad. No sorprende que su sistema de testing incorpore características que se alinean perfectamente con BDD:
Sistema de testing built-in sin dependencias externas (no necesitas instalar nada adicional).
Sintaxis moderna con describe() e it() para expresar comportamiento.
Seguridad de permisos por defecto (los tests se ejecutan en un sandbox; solo permites lo que necesitas).
TypeScript out-of-the-box sin configuración.
Esto convierte a Deno en una plataforma ideal para practicar BDD sin fricción.
Iniciemos un proyecto de prueba
Crearemos esta estructura de directorios para ir creando los archivos necesarios para una prueba simple.
/** * Simple calculator utilities used in examples and tests. * * Esta clase es intencionalmente pequeña y tiene métodos básicos * para demostrar pruebas unitarias en estilo BDD con Deno. */ exportclassCalc { /** * Suma dos números. * @parama - primer sumando * @paramb - segundo sumando * @returns la suma de `a` y `b` */ publicsum(a: number, b: number): number { return a + b; }
/** * Resta dos números (a - b). * @parama - minuendo * @paramb - sustraendo * @returns la resta de `a` menos `b` */ publicsubtract(a: number, b: number): number { return a - b; }
/** * Divide dos números (a / b). * * Lanza un error cuando `b` es 0 o no es un número válido (NaN), * para evitar divisiones inválidas. * * @parama - dividendo * @paramb - divisor (no debe ser 0 ni NaN) * @returns el resultado de `a / b` * @throws {Error} si `b` es 0 o NaN */ publicdiv(a: number, b: number): number { if (b === 0 || Number.isNaN(b)) { thrownewError("No se puede dividir por 0"); } return a / b; } }
A continuación, veremos cómo se implementan los tests unitarios sobre la clase Calc usando el estilo BDD que nos ofrece Deno y las dependencias de @std/testing y @std/expect.
Para ejecutar todos los tests, usamos el comando deno test. Este buscará y correrá todos los archivos de test en el proyecto, generando una salida similar a la siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
running 4 tests from ./test/Calc.test.ts Test unitarios sobre la clase Calc ... sum - casos adicionales (BDD) ... suma números positivos ... ok (1ms) sum - casos adicionales (BDD) ... ok (1ms) subtract - casos adicionales (BDD) ... resta números positivos ... ok (0ms) subtract - casos adicionales (BDD) ... ok (0ms) div - validaciones ... debería fallar dividiendo por 0 ... ok (1ms) debería dividir correctamente 8/2=4 ... ok (0ms) div - validaciones ... ok (1ms) Test unitarios sobre la clase Calc ... ok (2ms)
ok | 4 passed | 0 failed (10ms)
En este bloque de código, estamos definiendo la estructura de nuestros tests utilizando el estilo BDD que nos proporciona Deno:
describe("Test unitarios sobre la clase Calc", ...): Este es el bloque principal que agrupa todos los tests relacionados con nuestra clase Calc. Funciona como un contenedor o una suite de pruebas.
describe("sum - casos adicionales (BDD)", ...): Dentro de la suite principal, creamos un sub-grupo específico para las pruebas del método sum. Esto ayuda a organizar y contextualizar los tests.
it("suma números positivos", ...): Aquí definimos un caso de prueba concreto. La descripción "suma números positivos" deja claro cuál es el comportamiento que estamos validando.
Dentro del it, creamos una instancia de Calc.
Usamos expect(calc.sum(2, 3)).toEqual(5); para afirmar que el resultado de llamar a sum(2, 3) es igual a 5. La función expect viene de la librería @std/expect y nos ofrece una forma legible y expresiva de hacer aserciones.
describe("subtract - casos adicionales (BDD)", ...): De manera similar, creamos otro sub-grupo para el método subtract, manteniendo nuestros tests bien organizados.
describe("div - validaciones", ...): Finalmente, un grupo para el método div. Aquí se incluyen dos casos de prueba interesantes:
it("debería fallar dividiendo por 0", ...): Este test verifica que el código maneja los errores correctamente. Usamos expect(() => calc.div(1, 0)).toThrow(...) para asegurar que, al intentar dividir por cero, se lance un error con el mensaje esperado. Nota que la llamada a la función que debe fallar se envuelve en una función flecha () => ....
it("debería dividir correctamente 8/2=4", ...): Un test para el “camino feliz” o el caso de uso normal, asegurando que la división funciona como se espera.
Esta estructura jerárquica con describe e it no solo ejecuta el código, sino que también lo documenta de una manera que es fácil de leer y entender para cualquier persona en el equipo.
Ahora, profundicemos en cómo se estructuran y ejecutan los tests. La función describe es fundamental, ya que actúa como un agrupador que define un ámbito o scope para un conjunto de pruebas relacionadas. Dentro de este ámbito, podemos usar “hooks” (funciones especiales) para controlar el ciclo de vida de nuestros tests.
Estos son los hooks principales:
beforeAll: Se ejecuta una sola vez antes de que comiencen todos los tests dentro de su describe. Es ideal para preparar un estado inicial que no cambiará, como levantar un servidor o conectar a una base de datos de prueba.
beforeEach: Se ejecuta antes de cada test (it) dentro de su describe. Perfecto para resetear el estado entre pruebas, como limpiar una tabla de la base de datos o crear una nueva instancia de una clase.
afterEach: Se ejecuta después de cada test (it). Se usa para tareas de limpieza que deben ocurrir después de cada prueba, como borrar archivos temporales o cerrar una conexión.
afterAll: Se ejecuta una sola vez después de que todos los tests dentro de su describe hayan finalizado. Ideal para la limpieza final, como detener el servidor o cerrar la conexión a la base de datos.
La magia ocurre cuando anidamos bloques describe, ya que los hooks del ámbito exterior también se aplican a los ámbitos interiores.
Para ilustrar esto, veamos el siguiente código de ejemplo:
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from"@std/testing/bdd";
describe("Test - scope y hooks", () => { beforeAll(() => { console.log("Before All 1 (Scope Padre)"); });
afterAll(() => { console.log("After All 1 (Scope Padre)"); });
beforeEach(() => { console.log("Before Each 1 (Scope Padre)"); });
afterEach(() => { console.log("After Each 1 (Scope Padre)"); });
it("should test 1", () => { console.log("Test 1"); });
describe("Test - Scope Anidado", () => { beforeAll(() => { console.log("------> Before All 2 (Scope Anidado)"); });
afterAll(() => { console.log("------> After All 2 (Scope Anidado)"); });
beforeEach(() => { console.log("------> Before Each 2 (Scope Anidado)"); });
afterEach(() => { console.log("------> After Each 2 (Scope Anidado)"); });
it("should test 2", () => { console.log("------> Test 2"); }); }); });
La salida de esta ejecución revela el orden exacto en que Deno ejecuta cada bloque. Desglosemos lo que sucedió:
Salida: Orden de Ejecución
Test - scope y hooks ... ------- output ------- Before All 1 (Scope Padre) ----- output end ----- should test 1 ... ------- output ------- Before Each 1 (Scope Padre) Test 1 After Each 1 (Scope Padre) ----- output end ----- should test 1 ... ok (0ms) Test - Scope Anidado ... ------- output ------- ------> Before All 2 (Scope Anidado) ----- output end ----- should test 2 ... ------- output ------- Before Each 1 (Scope Padre) ------> Before Each 2 (Scope Anidado) ------> Test 2 ------> After Each 2 (Scope Anidado) After Each 1 (Scope Padre) ----- output end ----- should test 2 ... ok (0ms) ------- output ------- ------> After All 2 (Scope Anidado) ----- output end ----- Test - Scope Anidado ... ok (0ms) ------- output ------- After All 1 (Scope Padre) ----- output end ----- Test - scope y hooks ... ok (1ms)
ok | 1 passed (3 steps) | 0 failed (3ms)
Inicio del Scope Padre: Se ejecuta beforeAll del scope padre (Before All 1).
Ejecución del primer test:
Se ejecuta beforeEach del padre (Before Each 1).
Se ejecuta el cuerpo del Test 1.
Se ejecuta afterEach del padre (After Each 1).
Inicio del Scope Anidado:
Se ejecuta el beforeAll del scope anidado (Before All 2).
Ejecución del segundo test (anidado):
Se ejecuta beforeEach del padre (Before Each 1), porque el test anidado está dentro de su ámbito.
Se ejecuta beforeEach del scope anidado (Before Each 2).
Se ejecuta el cuerpo del Test 2.
Se ejecuta afterEach del scope anidado (After Each 2).
Se ejecuta afterEach del padre (After Each 1).
Fin del Scope Anidado: Se ejecuta afterAll del scope anidado (After All 2).
Fin del Scope Padre: Se ejecuta afterAll del scope padre (After All 1).
Como puedes ver, la función describe no solo agrupa tests, sino que crea una jerarquía. Los hooks del describe padre “envuelven” a los hooks y tests de los describe hijos, permitiendo crear configuraciones y limpiezas complejas y ordenadas. Esto es clave para escribir tests mantenibles y escalables.
Palabras al Cierre
Espero que esta guía te haya dado una buena base para empezar a escribir tests unitarios robustos y expresivos en Deno usando BDD. Esta metodología no solo mejora la calidad de tu código, sino que también ayuda a que otros desarrolladores entiendan rápidamente el objetivo de cada test y lo que se busca lograr.
Si hay algún otro tema sobre Deno o testing que te gustaría que explorara, ¡déjamelo saber en los comentarios!
Referencias
Aquí tienes algunos enlaces para profundizar en los conceptos que hemos cubierto:
Deno Manual - Testing: La documentación oficial de Deno sobre cómo escribir y ejecutar tests.
¿Qué es BDD? (Cucumber.io): Una excelente introducción a los principios y prácticas de Behavior-Driven Development por parte de uno de los referentes en el área.
Cuando me enfrento a un nuevo proyecto, ya sea una API para una aplicación móvil o un backend para un sitio web, siempre busco herramientas que me hagan la vida más simple y fácil. No quiero mantener configuraciones complejas ni enfocar esfuerzos a optimizar código con un framework pesado que consuma más recursos de los necesarios. Mis prioridades son simples: quiero que sea rápido, ligero y que se integre sin problemas con Deno, que es el entorno de ejecución de TypeScript que he adoptado para casi todo.
Y ahí es donde entra Hono.
Descubrí Hono buscando una alternativa moderna a Express.js (un framework que usé durante años en el mundo de Node.js). Aunque Express es casi de facto el framework web para node.js, sentía que se estaba quedando desactualizado no encajaba del todo con la simplicidad y seguridad que me ofrecía Deno. Necesitaba algo que se sintiera más nativo, más ágil y moderno.
Lo que me convenció de Hono fue precisamente eso. Es increíblemente liviano, casi no añade overhead a mi aplicación, y su rendimiento es espectacular. Pero la verdadera joya de la corona, para mí, es su versatilidad. Aunque mi ecosistema principal es Deno, Hono me da la tranquilidad de saber que mi código es portable. Si el día de mañana decido mover una API a Cloudflare Workers para que corra en el edge (más cerca de los usuarios), o si un cliente me pide desplegarla en un servidor tradicional con Node.js, puedo hacerlo con cambios mínimos. Esa libertad es algo que valoro muchísimo.
Como vengo del mundo de Express, empezar con Hono fue muy sencillo. La forma de definir rutas, gestionar peticiones (req) y respuestas (res) y usar middlewares es tan intuitiva y familiar que no sentí que hiciera un esfuerzo adicional, es decir, si vienes del mundo Express, la curva de aprendizaje es rápida.
En este post quiero contarte, desde mi experiencia, por qué Hono se ha convertido en mi framework de cabecera para casi cualquier proyecto web que involucre un backend.
Manos a la Obra: Tu Primer Proyecto con Hono y Deno
Ya te conté por qué me gusta Hono, ahora vamos a la práctica para que veas lo rápido que es empezar.
Primero, abre tu terminal, crea una carpeta para el proyecto y entra en ella:
1
mkdir mi-proyecto-hono && cd mi-proyecto-hono
Deno tiene un comando muy práctico para inicializar proyectos que nos ahorra bastante trabajo. Usaremos el siguiente:
1
deno init
Este comando te creará una estructura básica con un main.ts y un deno.json. Ahora, para instalar Hono, agregamos la dependencia en el archivo deno.json y luego simplemente lo importamos en nuestro main.ts. Deno lo descargará y cacheará la primera vez que ejecutes el código.
Puedes agregar Hono como dependencia usando JSR de la siguiente forma, pero debes editar el archivo deno.json para corregir el alias en el import.
1
deno add jsr:@hono/hono
El corazón de tu proyecto - deno.json
Antes de ver el código, echemos un vistazo al deno.json. Aquí es donde Deno gestiona las dependencias y tareas. Para usar Hono, podemos añadirlo a la sección imports o usar la línea de comandos de deno para agregar la dependencia:
1 2 3 4 5 6 7 8
{ "imports":{ "hono":"jsr:@hono/hono@^4.10.3"// <-- ojo con el alias }, "tasks":{ "start":"deno run --allow-net main.ts" } }
Un detalle importante es que, como ves, Hono está disponible en JSR (el nuevo registro de paquetes de JavaScript), lo que hace que manejar las versiones sea muy limpio.
Primer controlador
Ahora sí, el código de main.ts. Es tan simple y elegante como esto:
Define una ruta para el método GET en la URL raíz (/).
Cuando alguien visita esa ruta, le devuelve el texto “Hello Hono!”.
Finalmente, Deno.serve inicia el servidor.
Para ponerlo en marcha, usamos la tarea que definimos en deno.json:
1
deno task start
Como Deno es seguro por defecto, te pedirá permiso para acceder a la red. ¡Y listo! Ya tienes un servidor corriendo. Si usas una herramienta como httpie, verás esto:
1 2 3 4 5 6
$ http :8000/
HTTP/1.1 200 OK ...
Hello Hono!
¿Y si quiero devolver JSON?
Fácil. Hono tiene un método específico para eso. Simplemente cambia c.text() por c.json():
El resultado ahora será una respuesta JSON perfecta:
1 2 3 4 5 6 7 8 9
$ http :8000/
HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 ...
{ "message": "Hello Hono!" }
En solo unos minutos, hemos montado un servidor web funcional. Esta simplicidad y rapidez es lo que me terminó de convencer de la combinación de Hono y Deno.
¿ Cómo cambiar el puerto del servidor ?
Esto me costó pillarlo en su momento, así que dejo aquí cómo se hace:
1
Deno.serve({ port: 8080 }, app.fetch);
Revisemos algunos middlewares útiles
logger
Un middleware bien útil es el de logger, permite registrar (o loggear) lo que entra y lo que sale a Hono (obviamente depende de la ubicación en el archivo principal de la aplicación.)
Con esta configuración se puede obtener este tipo de logs.
1 2 3
Listening on http://0.0.0.0:8000/ (http://localhost:8000/) <-- GET / --> GET / 200 0ms
etag
La cabecera o header ETag (que viene de Entity Tag o “etiqueta de entidad”) es básicamente una huella digital o un identificador único que el servidor le asigna a una versión específica de un recurso (como una página, una imagen o un archivo JSON).
Imagina que tienes un archivo en tu servidor. Cada vez que modificas y guardas ese archivo, su ETag cambia. Es como el número de versión de ese recurso en un momento exacto.
¿Y para qué sirve?
Su propósito principal es hacer que el cacheo sea mucho más eficiente y ahorrar ancho de banda. El flujo es el siguiente:
Primera Visita: Cuando tu navegador pide un recurso (ej: styles.css) por primera vez, el servidor se lo envía completo y, además, incluye el header ETag con su “huella digital”. Por ejemplo: ETag: “v1-a2b3c4d5”.
El Navegador Guarda la Huella: Tu navegador guarda el archivo styles.css en su caché y también anota su ETag.
Siguientes Visitas: La próxima vez que necesite ese mismo archivo, el navegador no lo pide a ciegas. En su lugar, le pregunta al servidor: “Oye, tengo una versión de styles.css con la huella v1-a2b3c4d5. ¿Sigue siendo la buena?”. Esto lo hace enviando un header llamado If-None-Match: “v1-a2b3c4d5”.
El Servidor Decide:
Si la “huella digital” del archivo en el servidor coincide con la que envió el navegador, significa que el archivo no ha cambiado. El servidor responde con un 304 Not Modified. Este mensaje es muy liviano, no contiene el archivo, y básicamente le dice al navegador: “Todo sigue igual, usa la copia que ya tienes en tu caché”.
Si la “huella digital” no coincide, significa que el archivo fue actualizado. El servidor responde con un 200 OK, envía la nueva versión del archivo y, por supuesto, su nuevo ETag.
En resumen, el ETag permite al navegador evitar volver a descargar recursos que no han cambiado, haciendo que la navegación sea mucho más rápida y consuma menos datos. Además ayuda a las capas superiores de la aplicación a tomar mejores decisiones respecto del cache de la aplicación.
Finalmente esta será la respuesta donde aparece una cabecera con el etag.
1 2 3 4 5 6 7 8 9 10 11
http :8000/ HTTP/1.1 200 OK content-length: 25 content-type: application/json date: Tue, 28 Oct 2025 03:25:31 GMT etag: "6e488564c25da2b4d7326d5ebfa92e957af61dcb" vary: Accept-Encoding
{ "message": "Hello Hono!" }
cors
Probablemente, uno de los “problemas” más comunes al empezar a desarrollar una API es toparse con un error de CORS en el navegador.
Imagina que los sitios web son como reinos amurallados. Por seguridad, un reino (tudominio.com) no puede simplemente enviar un mensajero a pedirle recursos a otro reino (api.otrodominio.com). Esta regla se llama Política del Mismo Origen (Same-Origin Policy) y es fundamental para la seguridad en la web.
El middleware de CORS de Hono es el pasaporte diplomático que nos permite configurar qué “reinos” externos tienen permiso para comunicarse con nuestra API.
Por ejemplo, una configuración de CORS muy específica y segura podría ser así:
1 2 3 4 5 6 7 8 9 10 11
import { Hono } from'hono'; import { cors } from'hono/cors';
Analicemos las reglas que hemos definido en este bloque:
app.use('/api/*', ...): Primero, le indicamos a Hono que aplique este middleware únicamente a las rutas que comiencen con /api/. Esto es muy práctico para tener políticas de seguridad distintas para diferentes partes de tu aplicación.
origin: 'http://example.com': La regla más importante. Se especifica que solo las peticiones que vengan desde http://example.com están permitidas.
allowMethods: [...]: Define qué métodos HTTP están autorizados (POST, GET, OPTIONS). Un DELETE sería bloqueado.
allowHeaders: [...]: Una lista blanca de las cabeceras HTTP que el cliente tiene permitido enviar.
exposeHeaders: [...]: Permite que el código JavaScript del cliente pueda leer cabeceras de la respuesta que, por defecto, el navegador oculta por seguridad, como Content-Length.
maxAge: 600: Una optimización de rendimiento. Indica al navegador que puede guardar en caché el resultado de la petición de “verificación” (preflight) durante 600 segundos (10 minutos).
credentials: true: Esencial si tu frontend necesita enviar credenciales como cookies o cabeceras de autenticación (Authorization).
En resumen, este middleware te da un control muy granular para configurar una política de CORS segura, una práctica mucho más recomendable que simplemente abrir el acceso a todo el mundo con origin: '*'.
Palabras al Cierre
Como hemos visto en este recorrido, Hono se presenta como una alternativa muy atractiva en el ecosistema de JavaScript y TypeScript. Su filosofía minimalista, su rendimiento y, sobre todo, su capacidad para adaptarse a cualquier entorno de ejecución, lo convierten en una herramienta que vale la pena tener en nuestro arsenal.
Personalmente, la combinación de la simplicidad de Hono con la seguridad y el entorno moderno de Deno ha hecho que desarrollar APIs vuelva a ser una experiencia ágil y productiva.
Pero esto es solo el comienzo. Este artículo es el primero de una serie donde exploraremos a fondo el ecosistema de Deno y Hono. En las próximas entregas, veremos temas un poco más avanzados como:
Creación de nuestros propios middlewares.
Helpers de Hono
Rutas avanzadas y manejo de grupos.
Estrategias de testing para nuestras APIs.
Te invito a que experimentes con lo que hemos visto hoy y, por supuesto, a que te mantengas atento para los próximos artículos de la serie.
Referencias Útiles
Aquí te dejo una lista de enlaces que te serán de gran ayuda para profundizar en Hono y Deno:
Si te mueves en el mundo de JavaScript, seguro que Node.js es tu pan de cada día. Pero, ¿has oído hablar de Deno? Y no, no es solo “Node” al revés (aunque el guiño es genial). Deno es un runtime para JavaScript y TypeScript creado por el mismísimo Ryan Dahl, el padre de Node.js.
Imagínate que creas algo increíble, pero con el tiempo te das cuenta de que podrías haberlo hecho mejor. Eso es Deno para Ryan Dahl. Es su oportunidad de redimirse y arreglar lo que él considera los “errores de diseño” de Node.js. El resultado es un entorno de ejecución más moderno, seguro y con una experiencia de desarrollo que te va a sorprender.
¿Qué hace a Deno tan especial?
¡A programar desde el minuto cero!
Una de las cosas que más me gustan de Deno es que viene con todo lo que necesitas para empezar a trabajar. ¿Cansado de configurar ESLint, Prettier, Jest y un montón de dependencias de desarrollo cada vez que empiezas un proyecto?
Deno incluye out of the box:
Un formateador de código (deno fmt): Para que tu código siempre luzca impecable y consistente.
Un linter (deno lint): Para detectar problemas y mantener la calidad del código.
Un runner de tests (deno test): Con soporte para tests unitarios y de integración, al estilo BDD (como Jest) o al estilo de Deno con Deno.test.
Soporte nativo para TypeScript: Sin necesidad de instalar tsc ni configurar tsconfig.json. Escribes en TypeScript y Deno se encarga del resto.
Un repositorio de librerías (JSR), Deno tiene su propio ecosistema de dependencias llamado JSR. Pero en las últimas versiones ahora soporta librerías del ecosistema de NPM.
Esto significa menos tiempo configurando y más tiempo programando. ¡Productividad al máximo desde el primer momento! Toda la configuración del entorno queda en el archivo deno.json y de esta forma es fácil de transportar, lo subes al repositorio y todo tu equipo lo usa (y lo va mejorando).
Seguridad: Tú tienes el control
Otro de los pilares de Deno es la seguridad. A diferencia de Node.js, donde un script puede acceder a tu sistema de archivos, a la red o a las variables de entorno sin que te des cuenta, Deno ejecuta el código en un sandbox.
Por defecto, un script de Deno no tiene permisos para hacer nada “peligroso”. Si necesita acceder a la red, al sistema de archivos o a cualquier otra cosa, tienes que dárselo explícitamente con flags al ejecutar el comando:
1 2 3 4 5 6 7 8
# Permite leer del sistema de archivos deno run --allow-read mi_script.ts
# Permite conexiones de red deno run --allow-net mi_script.ts
# ¡Permite todo! (Úsalo con cuidado) deno run -A mi_script.ts
Este modelo de permisos explícitos te da un control total sobre lo que tus dependencias pueden y no pueden hacer, evitando sorpresas desagradables.
Gestión de dependencias
En Deno, no existe la carpeta node_modules ni el archivo package.json. Las dependencias se importan directamente desde una URL, como en el navegador:
La forma de hacer los imports con las urls en los archivos TS, a mi modo de ver, es poco mantenible. Se hicieron varios esfuerzos para que sea una vía simple de mantener, pero al parecer llegaron a algo como un administrador de dependencias centralizado en el archivo deno.json. En las últimas versiones, además, agregaron soporte para incluir dependencias del ecosistema de NPM, de esta forma, se amplió la cantidad de librerías que puedes usar.
Si vas a usar dependencias de NPM, a veces se genera un archivo package.json muy básico con el propósito de agregar dependencias solamente.
Deno descarga y cachea las dependencias la primera vez que se ejecuta el script. Si quieres asegurarte de que las dependencias no cambien, puedes usar un archivo deno.json (o deno.jsonc) para gestionar las versiones, de forma similar a como lo harías con un package.json, pero mucho más simple.
Un ejemplo rápido
La mejor forma de entenderlo es probándolo. ¡Vamos a crear un pequeño proyecto!
1. Instalación
Deno está disponible para la mayoría de los sistemas operativos. Pásate por la página de instalación, busca tu SO y sigue los pasos.
2. Inicializando el proyecto
Una vez instalado, crear un proyecto es tan fácil como esto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
$ mkdir mi-proyecto-deno && cd mi-proyecto-deno $ deno init
✅ Project initialized
Run these commands to get started
# Run the program deno run main.ts
# Run the program and watch for file changes deno task dev
# Run the tests deno test
¡Y ya está! Deno te crea tres archivos:
deno.json: El corazón de tu proyecto, similar al package.json.
main.ts: Tu punto de entrada.
main_test.ts: Un archivo de ejemplo para tus tests.
3. El código
El archivo main.ts que se genera es súper simple:
1 2 3 4 5 6 7 8
exportfunctionadd(a: number, b: number): number { return a + b; }
// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts if (import.meta.main) { console.log("2 + 3 =", add(2, 3)); }
Puedes ejecutarlo con deno run main.ts y verás en la consola:
1
2 + 3 = 5
4. Los tests
Y el archivo de test, main_test.ts, no se queda atrás en simplicidad:
Ejecútalo con deno test y verás que todo funciona como la seda:
1 2 3 4 5
Check file:///Users/pcollaog/mi-proyecto-deno/main_test.ts running 1 test from ./main_test.ts addTest ... ok (0ms)
ok | 1 passed | 0 failed (3ms)
Conclusión
Como ves, Deno no es solo un capricho. Es una propuesta seria que replantea el desarrollo del lado del servidor con JavaScript/TypeScript, poniendo el foco en la seguridad, la simplicidad y una experiencia de desarrollo moderna y sin fricciones.
Si vienes de Node.js, la curva de aprendizaje es mínima y las ventajas son enormes. Menos configuración, más seguridad y un conjunto de herramientas unificado que te hará la vida más fácil.
¿Mi recomendación? Dale una oportunidad. Puede que te sorprendas y encuentres a tu nuevo mejor amigo para tus proyectos de backend.
¿Y tú, ya has probado Deno? ¡Cuéntame tu experiencia en los comentarios!
En un próximo artículo, profundizaré en mi experiencia con los frameworks web más populares para Deno.
CommitLint es una herramienta que ayuda a adoptar una convención para escribir commits de forma estandarizada. esta convención se llama Conventional Commits y esta relacionada con un estándar a la hora de versionar artefactos llamada Semantic Versioning.
La especificación de Conventional Commits es una convención sobre cómo escribir los mensajes de commits (específicamente el título del commit). Además nos provee de un conjunto sencillo de reglas para crear un CHANGELOG de commits explícito; lo que hace más fácil escribir herramientas encima del historial. Esta convención encaja con SemVer, al describir en los mensajes de los commits las funcionalidades, parches y cambios de ruptura hechos.
El mensaje del commit debe ser estructurado de la siguiente manera:
1 2 3 4 5
<tipo>(ámbito opcional): <descripción>
[cuerpo opcional]
[nota(s) al pie opcional(es)]
Por ejemplo:
1 2 3 4 5
fix(api): se corrige validacion de email
Se agregan nuevos dominios a la validación del correo
Issue #2
Los tipos que más se utilizan en Conventional Commits son los siguientes:
feat: Se agrega una nueva característica
fix: Se corrige algún problema
chore: Se modifican archivos que rodean el código fuente (configuraciones, cambios de versión)
refactor: Refactorización del código
docs: se agrega o modifica documentación
test: Se agrega o modifican tests
Los tipos relevantes a la hora de generar valor en el CHANGELOG son los feat y los fix, en base a esos tipos se construye de forma automática la bitácora de cambios relevantes (útil al usar standard-version, leer mas adelante)
Instalar commitlint
1
yarn add -D @commitlint/{cli,config-conventional}
Si estas usando commonsjs debes cambiar el export default por module.exports. debes revisar el atributo type dentro del archivo package.json.
Ejecuta el siguiente comando para crear el archivo commitlint.config.js
Demás esta decir que puedes extender la configuración y acomodarla a tus necesidades, sólo considerar que las herramientas que se basan en estas convenciones también deberás adaptarlas y en otros casos es posible que no te sirvan.
husky
Husky es una herramienta que permite gestionar de forma simple git-hooks y que nos permite que cada vez que hagamos un commit, por ejemplo, corra los test unitarios o que nos revise si el commit message cumple con la convención de conventional commits (que es nuestro caso).
1 2 3 4 5
yarn add -D husky yarn husky init
npm pkg set scripts.commitlint="commitlint --edit" echo"yarn commitlint \${1}" > .husky/commit-msg
El ultimo comando (el echo), genera un archivo cuyo contenido es la ejecución de commitlint y será ubicado en el directorio de los hooks de husky.
Nota: Cuando ejecutas el comando yarn husky init este te genera un directorio llamando .husky y dentro quedará un archivo llamado pre-commit con el siguiente contenido:
1
yarn test
Esto quiere decir que antes de que se realice el commit, husky ejecurata el comando yarn test y si este comando sale sin problemas, se efectuará el commit, en caso contrario los archivos no serán agregados al historial.
standard version
Otra herramienta que nos puede ayudar a mejorar la forma en que realizamos los releases, es una utilidad llamada standard-version (es algo vieja pero funciona perfectamente). Según su propia descripción:
Una utilidad para el versionamiento usando semver y generación de CHANGELOG potenciados por conventional commits.
1
yarn add -D standard-version
Una vez agregada la librería se debe agregar un comando en la sección de scripts en el package.json (sólo para mayor comodidad).
Con eso ya puedes usar los comandos release y pre-release, el primero para concretar el release y el segundo es para ver como quedará de forma verbosa pero sin afectar el repositorio. Por ejemplo:
yarn run v1.22.19 $ standard-version --dry-run ✔ bumping version in package.json from 1.0.0 to 1.1.0 ✔ created CHANGELOG.md ✔ outputting changes to CHANGELOG.md
--- ## 1.1.0 (2024-04-22)
### Features
* se agrega busqueda de app por su id * se agrega funcion para eliminar aplicacion * se agrega persistencia (find,delete,create) de app * se agrega persistencia de eventos por app * se agrega validacion de esquema para applicacion * se agregan controladores para aplicaciones y logs
### Bug Fixes
* **ci:** se corrige nombre de la imagen mongodb * **ci:** se corrige script para test, falta instalar deps * se corrigen test unitarios * se incluye close connection ---
✔ committing package.json and CHANGELOG.md ✔ tagging release v1.1.0 ℹ Run `git push --follow-tags origin develop` to publish
Nótese que el comando de pre-release nos indica exactamente lo que hará el comando release, donde hay que resaltar la linea bumping version in package.json from 1.0.0 to 1.1.0 que indica, en función de commits messages, califican para cambio de minor (feat agregados) o patch (fix agregados). Por ejemplo… si entre versiones solo tienes commits messages con fix cuando hagas el release, lo hara modificando el dígito del patch, por otro lado, si agregas un nuevo feature (feat) te modificará el minor en la versión.
Si requieres hacer un versionamiento manual debes ejecutar el siguiente comando:
1
yarn release --releas-as 1.1.1
Con eso fuerzas a que la versión sea la que indicas como argumento.
GitFlow
Si usas git-flow como modelo de ramas, te sugiero configurar standard-version para que no realice el tag una vez que hagas el release, de esta forma le delegas dicha función a git-flow. Les dejo el archivo de configuración que se llama .versionrc y debe estar ubicado en la raíz del proyecto:
1 2 3 4 5
{ "skip":{ "tag":true } }
Con esta configuración, puedes lanzar los comandos de release en la rama hotfix o release y luego finalizas las rama con git-flow.
Palabras al cierre
Espero les sirva este pequeño post sobre herramientas de desarrollo que utilizo en el día a día y sus configuraciones.
Se requiere un mecanismo global o centralizado basado en las excepciones, para poder manejar y traducir en errores entendibles hacia las integraciones (frontend y/o otros clientes).
Introducción
La gestión de excepciones en el sentido amplio de la palabra, se refiere a la captura de eventos de tipo errores que se producen en las aplicaciones durante su ejecución. Estos errores pueden ser de muchos tipos, desde fallas de red, errores en accesos a una base de datos, errores en tiempo de ejecución (runtime) o simplemente por validaciones de negocio.
El manejo de excepciones tiene como objetivo hacer más seguras y robustas las aplicaciones así como también tener un mecanismo para dar una respuesta elegante a los usuarios mejorando la confiabilidad y resistencia a fallas en las aplicaciones.
Usos
Manejo global de excepciones:
¿ Cuándo usarlo ?
Cuando se necesita un formato de respuesta de error consistente para toda la aplicación.
Para evitar exponer trazas de la aplicación con información relevante, de esta forma evitar posibles futuros atacantes.
Manejo de excepciones en el controlador:
Utilizar cuando se necesita una solución particular (distinta a la ofrecida por el manejo de errores globales), como por ejemplo, integraciones donde necesitamos adaptar nuestra respuesta.
Implementación
Para su implementación, Spring Boot nos provee algunas herramientas, las más comunes para este tipo de manejadores son @ControllerAdvice y de @RestControllerAdvice, el primero para cualquier tipo de controlador anotado con @Controller (MCV de Spring) y el segundo esta especializado en controladores de tipo Rest (aplica sólo a los controladores anotados como @RestController)
Para que estos componentes puedan funcionar correctamente y como su nombre lo indica, deben poder atrapar excepciones del tipo específico (jerarquía) que necesitemos manejar y hacer la interpretación de la excepciín, como por ejemplo: se requiere atrapar cualquier Exception del tipo IllegalArgumentException y convertirla en un bad request (error 400 de http).
Hablemos un poco de excepciones
Los manejadores de excepciones siempre actúan cuando una excepción es lanzada y no ha sido atrapada por la capa de controladores, éstas pueden ser lanzadas desde las capas de acceso a datos, directamente desde la capa de negocio o simplemente desde la capa de controladores (validación de entrada).
Aquí la discusión se vuelve un poco densa y depende mucho el cómo quieres trabajar tu jerarquía de excepciones, puede ser tan compleja como quieras (casos en que necesitas mucho detalle en la respuesta del manejador) o tan simple como mantener un par de excepciones que manejar e interpretar.
En un articulo anterior escribí acerca de tipos de excepciones, jerarquía y características el manejo de excepciones. Les dejo aquí el enlace a este artículo Checked y Unchecked Exception.
Una de las alternativas mas simples es utilizar Excepciones de tipo RunTimeException ya que tienen la característica de no ser atrapadas de forma imperativa o declarativa, de esta forma excepciones que son lanzadas en las capas inferiores saltan hasta el manejador de Excepciones, claro está, solo si son manejadas en dicho manejador.
Veamos un ejemplo
En SpringBoot tenemos un par de mecanismos para poder manejar excepciones y que nos permite evitar exponer el stack trace directo al usuario (nos da seguridad en caso de exponer algún dato sensible) y en su reemplazo nos permite modelar una salida elegante dependiendo de la excepción lanzada.
Para efectos prácticos en este artículo utilizaremos sólo excepciones de tipo Runtime ya que este tipo de excepciones suben por el stack sin ser capturadas por nuestro código hasta llegar al lugar donde queremos hacer el manejo de la excepción.
Rest Controller Advice
GlobalExceptionHandleres una clase que se anota con @RestControllerAdvice y es donde podremos hacer el manejode la excepción. Veamos un ejemplo a continuación:
En este primer ejemplo, se evidencia que se atrapan las exepciones especificas y mas altas de la jerarquía, NotFoundException, BadRequestException y UnauthorizedException, ademas para cada una de ellas hay un método disponible donde podremos por ejemplo hacer un log del error para tener el detalle de lo ocurrido y finalmente enviar hacia el consumnidor, el correspondiente codigo Http para cada caso. Nota: en este ejemplo el body de la respuesta es vacío.
Como podrán ver, en el handler (método) podrán manejar la excepción a conveniencia y podrán enviar el mensaje y codigo de error que necesiten hacia el consumidor del servicio.
Para finalizar les dejo un diagrama del funcionamiento del GlobalExceptionHandler con el ejemplo de 404 Not Found.
Si tienen alguna observación no duden en comentar.
En la mayoría de los paradigmas de programación, podemos encontrar sistemas que nos permiten auto organizar el código en pequeñas piezas o para incorporar librerías/bibliotecas de terceros a nuestro código. Al combinar todo lo anterior resulta una pieza de código más grande y compleja.
Desde un inicio Javascript utilizó el sistema de carga de módulos llamado CommonsJS (CJS) y es parte integral de Node.js hasta la version v8.5.0 donde se incorpora un nuevo sistema de carga de módulos, ESM. A partir de la version v13.2.0 de Node.js fue estabilizado e incorporado como un nuevo estándar.
¿ Por qué deberíamos usar ESM en AWS Lambdas ?
James Beswick (Principal Developer Advocate for the AWS Serverless Team) escribió un artículo titulado Using Node.js ES modules and top-level await in AWS Lambda ,donde detalla el por qué y en qué casos deberías usar ESM como cargador de módulos de Javascript en el contexto de un AWS Lambda. Uno de los motivos más importantes de usar ESM, es que la carga en frío de un lambda tarda casi un tercio en comparación con CommonsJS.
Dejo aquí la comparativa entre CJS y ESM donde en el p99 (carga en frío) la partida se reduce a un tercio mejorando el rendimiento en un 43,5%. Para esta prueba, todas las métricas de ESM salieron por debajo (mejores en tiempo) de las de CJS es simplemente marginal (alrededor de 2-5ms).
¿ Cómo usamos ESM en AWS Lambdas ?
Configuración del artefacto - package.json
Primero que todo se deben ajustar un par de atributos en el archivo package.json para indicar que el módulo es del tipo ESM.
package.json
1 2 3 4 5 6 7 8 9
{ "name":"serverless-lambda-esm", "version":"1.0.0-beta", "description":"Serverless Lambda ESM", "main":"src/Handler.js",// <-- Sera cargado como módulo "license":"UNLICENSED", "private":true, "type":"module",// <-- Este atributo debe tener el valor: module }
Con este ajuste le indicamos al cargador que trate los archivos como ES módulos según su extensión, es decir, con el atributo type y valor en module todos los archivos con extensión .js serán tratados como ESM. Si por alguna razón quieres mezclar ambos mundos debes hacerlo de forma explícita usando como extensión .cjs. Por el contrario, si no utilizas el atributo type o lo dejas con valor commonjs, todos los archivos con extension .js serán tratados como CJS y si quieres utilizar ESM estos deben tener extension .mjs.
El handler debe estar expuesto de esa forma para que sea cargado como ESM. Se puede apreciar que ya no usamos la sentencia require para cargar una dependencia externa (en esta caso luxon) y en su reemplazo utilizamos import.
Algunos detallitos que he ido aprendiendo en el camino y que no está muy explícito en la documentación, es que los imports de nuestros archivos, es decir, el código que está dentro del proyecto, deben ser cargados y nombrados con su extensión, dejo un ejemplo:
Handler.js
1 2
import { DateTime } from"luxon"; // <-- Sin la extension (lib) importServicefrom'./service/Service.js'; // <-- Con la extension
exportdefaultService; // <-- Así se exponen los módulos al código
Palabras al cierre
Espero les sea útil para sus desarrollos de lambdas con Node.js. Más adelante ire dejando nuevos artículos con ejemplos e ideas para implementar con AWS Lambdas. Pase y deje su comentario.
jEnv es una herramienta de linea de comandos que ayuda a mantener múltiples instalaciones de Java (versiones y/o sabores) y permite cambiar las versiones por linea de comandos para poder mantener diversos entornos de desarrollo.
Una de las funcionalidades interesantes de jEnv es que configura las variables de entorno de java JAVA_HOME al vuelo según una pequeña configuración que se agrega al proyecto. Más adelante veremos algunas gracias de jEnv.
Instalación
La instalación es bien sencilla y la pueden revisar en la página oficial de jEnv, acá dejo un resumen:
Reiniciamos el terminal y/o cargamos las nueva variables de entorno de nuestra shell y primero probamos que todo funcione correctamente con el siguiente comando:
1
$ jenv --version
Si el comando te responde con la versión de jEnv es porque esta todo correctamente instalado.
Configuración
Luego tenemos que agregar las diferentes versiones de java que tengas instaladas en tu sistema de la siguiente forma:
Una ves que hayas concluido la configuración de las diferentes versiones de Java de tu sistema, puedes listarlos con el siguiente comando:
1
$ jenv versions
Y aparecerá algo como esto:
1 2 3 4 5 6 7
system * 1.7 (set by /home/pcollaog/.jenv/version) 1.7.0.79 1.8 1.8.0.45 oracle64-1.7.0.79 oracle64-1.8.0.45
Ahora corresponde configurar cual de todas esas instalaciones será la que funcionará de forma global, es decir, la configuración por omisión de Java. En este ejemplo se configura la versión oracle64-1.7.0.79 como global.
1
$ jenv global oracle64-1.7.0.79
Tambien se puede configurar de forma mas genérico, es decir, la última versión de java 7, por ejemplo:
1
$ jenv global 1.7
Con esto siempre tomará la versión mas nueva de java 7 que tengan previamente configurada.
Activar la configuración automática de JAVA_HOME
Para delegar la configuración de las variables de entorno de java a jEnv, debemos activar el plugin export de la siguiente forma:
1
$ jenv enable-plugin export
Nota: para que funcione correctamente el plugin, se debe eliminar la configuración de las variables de entorno que se tengan en los perfiles de bash (.bashrc) , zsh (.zshrc) o de su shell favorita.
Luego deben reiniciar su terminal o volver a cargar los perfiles de su shell y probar:
1
$ echo $JAVA_HOME
Debería aparecer algo como lo que sigue:
1
/home/pcollaog/.jenv/versions/1.7
Y si vemos la versión de java debería aparecer algo como esto:
1 2 3 4
$ java -version java version "1.7.0_79" Java(TM) SE Runtime Environment (build 1.7.0_79-b15) Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
Configuración personalizada de Java
Si por alguna razón necesitas tener una versión especifica de Java en un proyecto de código fuente, debes configurar (estando dentro del directorio principal del proyecto) de la siguiente forma:
1
$ jenv local 1.7
Este comando creará un archivo en el directorio en el que te encuentras (directorio base de tu código fuente) llamado .java-version y cuyo contenido será la versión de java especificada. Con esto todos los subdirectorios (a partir de donde se encuentras este archivo) estarán configurados con la versión de java seleccionada. Si tienes correctamente configurado el plugin export, jEnv hará el trabajo sucio de configurar la variable JAVA_HOME.
En esta oportunidad les escribo sobre un nuevo namespace que apareció en Spring 3.1 (y me dí cuenta recién XD ) y sirve para configurar los beans haciendo uso de su constructor. Como su nombre lo indica este es un namespace que opera sólo sobre los constructores de beans y así permitir la inyección de beans o valores.
Les dejo una imagen de como se activa este nuevo namespace en el STS.
Veamos un ejemplo simple de cómo usar namespace C.
<!-- Definición del datasource de ejemplo --> <beanid="datasource"class="com.mchange.v2.c3p0.ComboPooledDataSource"> ... ... </bean>
Como pueden notar en el ejemplo, el namespace C permite configurar los constructores de los beans y tiene dos formas básicas, por valor y referencia. Para inyectar una instancia preconfigurada se debe usar el sufijo -ref para hacer alusión a que es una referencia. Si se desea inyectar un valor, sólo se usa el nombre del argumento del constructor.
Dado que varios de mis lectores me han solicitado un post sobre tipos de excepciones en Java, les dejaré un par de notas para que consideren al momento de diseñar soluciones y por su puesto el cómo manejar los errores. Aquí vamos!
Primero que todo, un par de definiciones básicas y características antes de partir.
Jerarquía de Excepciones
Esta es la jerarquía de excepciones de mas alto nivel que encontramos en Java.
Unchecked Exception
Generalmente este tipo de excepciones son lanzadas por la aplicación y se generan a partir de errores en tiempo de Runtime. Este tipo de excepciones representan errores en el código y que la aplicación no es capaz de controlar. Algunos de errores causados y que lanzan este tipo de excepciones, por ejemplo, argumentos inválidos pasados a un método (argumentos null pueden causar NullPointerException), otro error común son la excepciones del tipo IndexOutOfBoundsException y que son lanzadas cuando se quieren obtener elementos de una lista y el índice que se entrega está fuera del tamaño del arreglo. Como podrán ver, son errores de programación y que generarán defectos en momento de correr la aplicación (no así al compilar).
Unchecked runtime exceptions represent conditions that, generally speaking, reflect errors in your program’s logic and cannot be reasonably recovered from at runtime.
Gosling, Arnold and Holmes, The Java Programming Language
Las excepciones de tipo Unchecked son subclases que heredan desde RuntimeException. Además este tipo de excepciones no tienen la obligación de ser declaradas con la cláusula throws en la cabecera del método. Otra característica es que tampoco se tiene la obligación de atraparlas con un catch como se muestra en el ejemplo siguiente:
/** * Ejemplo de exception tipo {@link RuntimeException} * */ publicclassRuntimeDemo {
/** * Método principal */ publicvoidmainMethod() { methodThowsRuntimeException(); methodThowsRuntimeException2(); }
/** * Este método lanzará una excepción de tipo runtime no declarada en su * firma */ publicvoidmethodThowsRuntimeException() { thrownewExampleRuntimeException(); }
/** * Este método lanzará una excepción de tipo runtime está declarada en su * firma (no es obligación) pero deja mas claro al desarrollador las * excepciones que debería manejar con la API. * * @throws ExampleRuntimeException * en caso de error */ publicvoidmethodThowsRuntimeException2()throws ExampleRuntimeException { thrownewExampleRuntimeException(); }
/** * Clase de error tipo Runtime */ publicstaticclassExampleRuntimeExceptionextendsRuntimeException {
publicExampleRuntimeException() { super(); } } }
Checked Exception
Este tipo de excepciones representan condiciones inválidas en el contexto de la línea de ejecución y que están fuera del control de dicho contexto, como por ejemplo, problemas con la base de datos, problemas de red, acceso a los archivos. También pueden ser condiciones de ingreso al sistema en donde el sistema no tiene ninguna participación, como por ejemplo, ingresar un nombre de usuario y contraseña incorrectos.
Contexto de ejecución ó scope: Corresponde al las líneas de código que están encerradas en un bloque de código, como por ejemplo, un método, un try/catch, bloque estático, etc.
Este tipo de excepciones deben ser declaradas en la firma del método. Además deben ser atrapadas dentro de los bloques de código donde se invoque un método que contenga la clausula throws.
Todas las excepciones de este tipo son subclases que heredan desde Exception, como por ejemplo:
Otra característica de este tipo de excepciones es que existe una probabilidad de recuperación de la ejecución y el método puede realizar alguna acción correctiva y/o informativa (log) en el bloque catch o simplemente relanzar la excepción y confiar en que el método invocante la atrape y haga algo con ella.
Para enviar el stacktrace al sistema de log (systemout) pueden usar los métodos de Throwable y en específico al método printStackTrace.
/** * Clase de ejemplo para checked exceptions */ publicclassCheckedExample {
/** * Método que atrapa una checked exception */ publicvoidcatchCheckedException() { try { throwCheckedException(); } catch (ExampleCheckedException e) { // TODO: hacer algo en caso de error (recuperacion) } }
/** * Método que relanza la checked exception a un método superior * * @throws ExampleCheckedException * checked exception error */ publicvoidrethrowCheckedException()throws ExampleCheckedException { throwCheckedException(); }
/** * Clase que representa la excepcion de ejemplo */ privatestaticfinalclassExampleCheckedExceptionextendsException {
publicExampleCheckedException() { super(); }
} }
Error
Las excepciones de tipo Error son excepciones en las que el sistema no puede hacer nada con ellas, son clasificadas como errores irreversibles y que en su mayoría provienen desde la JVM, como por ejemplo: IOError, NoClassDefFoundError, NoSuchMethodError, OutOfMemoryError y VirtualMachineError por mencionar algunos de los errores.
Un poco de diseño y consejos para el manejo de excepciones
Para iniciar esta última parte, comenzaré con algunos ejemplos de malas prácticas con las que siempre nos encontramos cuando programamos, les dejo unas pocas:
1 2 3 4 5
try { //ejecución que lanza checked exceptions } catch (ExampleCheckedException e) { // No hace nada }
En el ejemplo de arriba claramente no se hace nada con la excepción dentro del try, lo recomendable es que si de verdad no vas a hacer nada con la excepción, al menos debes enviarla a tu sistema de Logger favorito con algún nivel de debug aceptable para poder revisar el log. Siempre se recomienda hacer algo en el bloque catch ya que ocurrió un error que debe ser controlado.
Es similar ejemplo 1 pero lo que se hace es relanzar la excepción atrapada hacia el método invocante. La recomendación es que nunca hagas eso ya que se presta para confusión al leer el código fuente y en la practica se estarían ejecutando dos bloques catch para la misma excepción (el directo y el del método invocante).
1 2 3 4 5
try { //ejecución que lanza checked exceptions } catch (Exception e) { // alguna lógica de negocio }
Es recomendable que nunca atrapen todas las excepciones en un bloque catch y básicamente porque uno pierde la noción de por qué se produjo la excepción. Además en ese bloque también se atrapan las excepciones de tipo Runtime y ya mencionamos que estas excepciones significan errores en tu programa y que deben ser depurados (no escondidos debajo de la alfombra). Lo mejor es atrapar cada una de las excepciones y darle un tratamiento a cada una, si necesitan agrupar usen la herencia/jerarquía de las excepciones.
1 2 3 4 5 6 7 8 9 10 11
try { //ejecución que lanza checked exceptions } catch (Exception e) { // alguna lógica de negocio
if (e instanceof BlaException ){
} elseif (e instanceof FooException) {
} elseif ..... }
Ese bloque de if’s compuestos se debe transformar en varios catch para cada una de las excepciones lanzadas. No usen un control de errores manual, es mejor usar las herramientas que te provee el lenguaje.
No abusar de las checked exceptions ya que hacen nuestro código confuso y poco mantenible. Si bien es cierto, es la herramienta que nos provee el lenguaje, no abusemos de ella y convirtamos los bloques catch en pseudo programas y rutinas anexas a la lógica de negocio (que es la que vale).
1 2 3
privatevoidfoo()throws Exception { // código de negocio }
Nunca lancen Exception como una excepción de su lógica de negocio y es que básicamente los catch están pensados en atrapar excepciones particulares y al lanzar Exception (de la mas alta jerarquía) jamás entrarás al bloque catch que corresponda y que pueda gestionar el error. Por otro lado el programador pierde la visibilidad de los errores particulares que debe gestionar.
Jamás se debe hacer esto, jamás!. Esto romperá todo tu programa ya que al lanzar la excepción RuntimeException esta llegará sin control a la capa mas alta provocando un error. Recuerden que ese tipo de excepciones son errores sin recuperación y justamente estamos tratando de hacer lo contrario gestionar los errores de lógica de negocio.
Mantener un árbol de excepciones
Esta sección del post quizás sea el más polémico ya que no hay receta perfecta para el manejo de excepciones y daré mis consejos (personales), puede que estén de acuerdo como puede que no.
Les recomiendo siempre mantener un árbol de excepciones que representen los errores (de negocio y de ejecución) de tu aplicación. Para que sea mas simple la mantención del árbol de excepciones, usen polimorfismo, herencia y todas las herramientas que ofrece OOP. En este punto siempre hay detractores de los árboles de excepciones con la excusa de su mantención.
Contraria a mi propuesta de manejo de errores, existen quienes mantienen sólo 1 excepción y a dicha excepción le agregan atributos y cuanta metadata puedan agregar. Qué se consigue finalmente con ese esquema de errores, es llenarte de IF por todos lados mirando los atributos que contiene la instancia de excepción y haciendo todo un control de errores manual.
Palabras al cierre
Quedan muchas cosas por mencionar de las excepciones y ahondar mucho mas en cómo diseñar y construir un árbol de excepciones, creo que sera materia para otro artículo. Demás esta decirles que esta abierta la discusión. Los comentarios bienvenidos sean.