Optional en Java

  ___       _   _               _    _________
/ _ \ _ __| |_(_)___ _ _ __ _| | / /_ _\ \
| (_) | '_ \ _| / _ \ ' \/ _` | | < < | | > >
\___/| .__/\__|_\___/_||_\__,_|_| \_\ |_| /_/
|_|

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:

  1. 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.
  2. 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.
  3. 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)
String valorNulo = 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"
String resultado1 = 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.
String resultado2 = optionalVacio.orElseGet(() -> "valor generado");

// Si el optional está vacío, lanza una excepción.
String resultado3 = optionalVacio.orElseThrow(() ->
new IllegalStateException("No se encontró el valor")
);

Transformando valores con map y flatMap

Aquí es donde Optional realmente brilla, permitiendo un estilo de programación funcional.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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) {
return this.exampleRepository.findById(id)
.orElseThrow(() -> new ExampleNotFoundException(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:

Test Unitarios con Deno

¿Qué es BDD?

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

  1. Claridad: Los tests se leen como especificaciones. Cualquiera (técnico o no) puede entender qué se está probando.
  2. Documentación actualizada: El comportamiento del código siempre está documentado en los tests; no se queda obsoleto como un README abandonado.
  3. Colaboración efectiva: Diseñadores, QA y desarrolladores pueden discutir el comportamiento esperado usando el mismo lenguaje.
  4. Menos bugs por ambigüedad: Al escribir primero el comportamiento esperado, se reducen las interpretaciones erróneas.
  5. 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.

Estructura de directorios
calc-project/
├── src/
│ └── Calc.ts
├── test/
│ └── Calc.test.ts
└── deno.json

El contenido del archivo deno.json es el siguiente. Recuerda que en este archivo es donde se declaran las dependencias y las tareas ejecutables.

1
2
3
4
5
6
7
{
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.15",
"@std/expect": "jsr:@std/expect@^1.0.17",
"@std/testing": "jsr:@std/testing@^1.0.16"
}
}

Ahora revisemos el contenido de los archivos del proyecto. El primero es una clase con 3 operaciones matemáticas simples: suma, resta y división.

Calc.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 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.
*/
export class Calc {
/**
* Suma dos números.
* @param a - primer sumando
* @param b - segundo sumando
* @returns la suma de `a` y `b`
*/
public sum(a: number, b: number): number {
return a + b;
}

/**
* Resta dos números (a - b).
* @param a - minuendo
* @param b - sustraendo
* @returns la resta de `a` menos `b`
*/
public subtract(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.
*
* @param a - dividendo
* @param b - divisor (no debe ser 0 ni NaN)
* @returns el resultado de `a / b`
* @throws {Error} si `b` es 0 o NaN
*/
public div(a: number, b: number): number {
if (b === 0 || Number.isNaN(b)) {
throw new Error("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.

Test unitarios sobre la clase Calc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { expect } from "@std/expect/expect";
import { describe, it } from "@std/testing/bdd";
import { Calc } from "../src/Calc.ts";

describe("Test unitarios sobre la clase Calc", () => {
describe("sum - casos adicionales (BDD)", () => {
it("suma números positivos", () => {
const calc = new Calc();
expect(calc.sum(2, 3)).toEqual(5);
expect(calc.sum(10, 20)).toEqual(30);
});
});

describe("subtract - casos adicionales (BDD)", () => {
it("resta números positivos", () => {
const calc = new Calc();
expect(calc.subtract(5, 3)).toEqual(2);
expect(calc.subtract(20, 10)).toEqual(10);
});
});

describe("div - validaciones", () => {
it("debería fallar dividiendo por 0", () => {
const calc = new Calc();
// Capturar el error de ejecución
expect(() => calc.div(1, 0)).toThrow("No se puede dividir por 0");
});

it("debería dividir correctamente 8/2=4", () => {
const calc = new Calc();
const result = calc.div(8, 2);
expect(result).toEqual(4);
});
});
});

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:

  1. 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.

  2. 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.

  3. 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.
  4. describe("subtract - casos adicionales (BDD)", ...): De manera similar, creamos otro sub-grupo para el método subtract, manteniendo nuestros tests bien organizados.

  5. 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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)
  1. Inicio del Scope Padre: Se ejecuta beforeAll del scope padre (Before All 1).
  2. 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).
  3. Inicio del Scope Anidado:
    • Se ejecuta el beforeAll del scope anidado (Before All 2).
  4. 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).
  5. Fin del Scope Anidado: Se ejecuta afterAll del scope anidado (After All 2).
  6. 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:

Hono: Framework para aplicaciones web

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:

1
2
3
4
5
6
7
8
9
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
return c.text("Hello Hono!");
});

Deno.serve(app.fetch);

¿Qué hace este código?

  1. Importa Hono.
  2. Crea una nueva instancia de la aplicación.
  3. Define una ruta para el método GET en la URL raíz (/).
  4. Cuando alguien visita esa ruta, le devuelve el texto “Hello Hono!”.
  5. 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():

1
2
3
app.get("/", (c) => {
return c.json({ message: "Hello Hono!" });
});

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.)

1
2
3
4
5
6
import { Hono } from "hono";
import { logger } from "hono/logger";

const app = new Hono();

app.use(logger());

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:

  1. 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”.
  2. El Navegador Guarda la Huella: Tu navegador guarda el archivo styles.css en su caché y también anota su ETag.
  3. 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”.
  4. 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.

Ahora la configuración para Hono:

1
2
3
4
5
6
import { Hono } from "hono";
import { etag } from "hono/etag";

const app = new Hono();

app.use(etag());

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';

app.use('/api/*', cors({
origin: 'http://example.com',
allowMethods: ['POST', 'GET', 'OPTIONS'],
allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests'],
exposeHeaders: ['Content-Length', 'X-Kuma-Revision'],
maxAge: 600,
credentials: true,
}));

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:

Deno: ¿Una alternativa real a Node.js?

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:

1
import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

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
export function add(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:

1
2
3
4
5
6
import { assertEquals } from "@std/assert";
import { add } from "./main.ts";

Deno.test(function addTest() {
assertEquals(add(2, 3), 5);
});

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.


Referencias

conventional commits y algunas herramientas de apoyo

¿ qué es conventional commits ?

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

1
$ echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

O agregar este archivo en la raíz del proyecto:

1
2
3
export default { 
extends: ['@commitlint/config-conventional']
};

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).

1
2
3
4
5
"scripts": {
...
"release": "standard-version",
"pre-release": "standard-version --dry-run"
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ yarn pre-release

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.

Les dejo los links de las referencias:

Manejo de excepciones en Spring Boot

Motivación

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cl.pcollaog.eh.handler;

import cl.pcollaog.eh.exception.BadRequestException;
import cl.pcollaog.eh.exception.NotFoundException;
import cl.pcollaog.eh.exception.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<Void> badRequestHandler(BadRequestException bre) {
LOGGER.error("badRequestHandler - message {}", bre.getMessage());
return ResponseEntity.badRequest().build();
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Void> notFoundExceptionHandler(NotFoundException nfe) {
LOGGER.error("notFoundExceptionHandler - message: {}", nfe.getMessage());
return ResponseEntity.notFound().build();
}

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<Void> unauthorizedExceptionHandler(UnauthorizedException ue) {
LOGGER.error("unauthorizedExceptionHandler - message: {}", ue.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}

En este segundo ejemplo, vamos a manipular la respuesta (vamos a suponer que la integración lo pide así) para cada caso.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package cl.pcollaog.eh.handler;

import cl.pcollaog.eh.exception.BadRequestException;
import cl.pcollaog.eh.exception.NotFoundException;
import cl.pcollaog.eh.exception.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<CustomResponse> badRequestHandler(BadRequestException bre) {
LOGGER.error("badRequestHandler - message {}", bre.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.BAD_REQUEST), bre.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<CustomResponse> notFoundExceptionHandler(NotFoundException nfe) {
LOGGER.error("notFoundExceptionHandler - message: {}", nfe.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.NOT_FOUND), nfe.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<CustomResponse> unauthorizedExceptionHandler(UnauthorizedException ue) {
LOGGER.error("unauthorizedExceptionHandler - message: {}", ue.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.UNAUTHORIZED), ue.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

public static class CustomResponse {

private final LocalDateTime date;

private final HttpStatus status;

private final String message;

public CustomResponse(HttpStatus status, String message) {
this.date = LocalDateTime.now();
this.status = status;
this.message = message;
}
// ... Omitiré los getter
}
}

Para el caso de NotFoundException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 404 
Connection: keep-alive
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:20:02 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:20:02.462356",
"message": "Entity not found",
"status": "NOT_FOUND"
}

Para el caso de BadRequestException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 400 
Connection: close
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:16:41 GMT
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:16:41.886069",
"message": "Invalid argument",
"status": "BAD_REQUEST"
}

Finalmente para el caso de UnauthorizedException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 401 
Connection: keep-alive
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:21:38 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:21:38.011316",
"message": "Invalid Token",
"status": "UNAUTHORIZED"
}

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.

AWS Lambda - ESM Node.js

Algunas generalidades previas

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.

Preparamos el Handler

Handler.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { DateTime } from "luxon";

/**
*
* @param {*} event
* @returns
*/
export const handler = async (event, context) => {
const date = DateTime.now().toFormat("yyyy-MM-dd");
console.log("Date: ", date);
console.log(JSON.stringify(event));

return {
statusCode: 200,
body: JSON.stringify({ message: "ok" })
};
};

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)
import Service from './service/Service.js'; // <-- Con la extension

Export de los módulos

Service.js
1
2
3
4
5
6
7
8
9
10
11

const Service = {
/**
* Funcion
*/
doSomething: async(event)=>{
console.log("Event: ", JSON.stringify(event));
}
};

export default Service; // <-- 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.

Enlaces de interés

Escuchando Subterranean Homesick Alien del disco OK Computer

Radiohead

Nuevo Comienzo

De vuelta

Hola, se que no he escrito en mucho tiempo, muchísimo tiempo.

Partiré diciendo que me cambie de sistema de blog desde Jekyll (ruby… puaj) a Hexo (nodejs) solo por salir de una tecnología que simplemente no va conmigo y tampoco le he dado el suficiente tiempo para entenderlo. Hexo es una plataforma para generar contenido estático (blog) a partir de archivos markdown, está desarrollado sobe NodeJS y todos sus componentes (plugins y themes) siguen la misma línea. Debo decir que hay muchos componentes que están bien des-actualizados y muchos de ellos con documentación en algo que parece ser Chino.

Ha pasado muchas cosas desde la última vez que escribí, tuvimos un estallido social en Octubre del 2019 y luego nos remeció una pandemia que nos estuvo encerrados un buen tiempo. De alguna forma ambos eventos forzaron al trabajo remoto y muchas empresas nos vimos obligados a adaptarnos a la nueva forma de trabajar y relacionarnos entre los diferentes equipos.

En Pandemia

En pandemia no había mucho que hacer (fuera de la casa) así que aplicamos creatividad y aprendí a hacer Pan Amasado y pan con Masa Madre (al igual que muchos). Esto debido a que en esos días salir a comprar el pan era de riesgo vital.

En mitad de la pandemia nos cambiamos de casa apenas se dió la oportunidad (los permisos de mudanza estaban cerrados). Ya instalados en el nuevo HQ comenzó la nueva aventura de colegio virtual. No fue fácil y menos para la Vale, no generó un vínculo con sus profesores a través de una pantalla. Por otro lado la Javi se adaptó de una forma increíble a los quehaceres del colegio.

Afortunadamente para nuestro rubro, la pandemia aceleró muchos procesos informáticos y como mencioné mas arriba, presionó para que el trabajo remoto se consolidara como LA nueva forma de trabajo. Muchas compañías necesariamente tuvieron que crear nuevos sistemas y aplicaciones para atender a sus clientes o darles la posibilidad de no ir a hacer un tramite presencial (no se podía), lo que de alguna forma benefició a muchas empresas de desarrollo.

Estando encerrado aprendí cosas muy básicas sobre mantenimiento de guitarras (no quiero decir lutheria porque estoy a años luz de ese conocimiento) para justamente echarles una “manito de gato” a mis guitarras. Fue tanto el entusiasmo que me armé una guitarra que compre en Amazon, un kit DIY Do it yourself estilo PRS.

Por supuesto que no fue fácil y creo que las dos etapas críticas fueron pegar el mástil al cuerpo y la “pintura”, en ese orden de complejidad. Digo “pintura” pero en realidad es una técnica de teñido de la madera con tinturas basadas en anilina y agua. Les dejo parte del proceso:

Ahora esa guitarra es una de mis favoritas por varias razones, tiene un sonido único como hecha para tocar metal, es cómoda y tiene un sustain exagerado. Por lo demás es escala 25” con 24 trastes lo que la hace distinta de las demás guitarras. Cosas que le faltan, aparte de un buen guitarrista XD, mejorar la entonación y cambiar el puente porque lo rompí y no hay repuesto. Afortunadamente la cuerda mas difícil de todas la G quedó perfectamente entonada para el calibre de cuerdas que usa esa guitarra.

Eso es todo por hoy… iré agregando mas contenido a mi blog que dicho sea de paso tiene 19 años (ni yo me lo creo).

Adiós 2017... bienvenido 2018

Un año más, que se va,
un año más, cuántos se han ido.
Un año más, que más da,
cuántos se han ido ya…

Hernán Gallardo Pavéz

Esta canción es casi un himno patrio de fin de año y no hay fiesta donde no se toque esta canción, que dicho sea de paso, ha sido interpretada por varias bandas. Aquí les dejo un link para que lean acerca de la historia de ésta canción.

Symbiose SpA y Tecnología

Este año ha sido particularmente extraño en lo que a negocios y pega se refiere. Tuvimos elecciones presidenciales y que durante lo que dura la “incertidumbre”, los proyectos y las inversiones en tecnología se aletargar paralizan bastante. Una vez pasada la “incertidumbre” (para bien o para mal) se vuelven a activar los proyectos. En Symbiose SpA hemos sentidos esos embates del mundo económico y político aunque no tengamos que ver con ninguno de los dos.

En lo técnico quizás lo mas novedoso ha sido programar en nuevos frameworks y lenguajes. Uno de los nuevos frameworks con los que he tenido que sufrir lidiar ha sido Angular + TypeScript. Solo les puedo comentar que este último lenguaje ha sido vilipendiado de la peor forma en el trabajo y básicamente por lo odioso que es el “compilador” y los mensajes de error que salen en la consola de desarrollo del browser (sólo si es que salen). Hace mucho tiempo que no rabiaba tanto con algo que me gusta hacer.

En comparación, usando AngularJS como framework, es bastante mas legible y decente los errores que salen en la consola y los errores típicos son los dolores de cabeza que te da Javascript, que de alguna u otra forma, muchos developers tenemos interiorizados.

Sin lugar a duras en temas tecnológicos el uso de contenedores ha sido protagonista en el desarrollo de microservicios y la aceptación de esta arquitectura que viene de hace hace un par de años. En este ámbito Docker ha sido una de las plataformas que se mantiene en la pelea de la “containerización” junto a Kubernetes.

El uso que le damos a diario a estas herramientas van de la mano con Continuous Integration y Continuous Delivery evitando así HH preciadas de los desarrolladores peleando contra los servidores de aplicación, bases datos y cuanta aplicación ofrezca pelea. Para resumir este punto, que mejor que utilizar la palabra AUTOMATIZACIÓN.

Para finalizar lo técnico, sigo acumulando horas de vuelo SpringFramework y toda sus componentes orientados a crear microservicios y ambientes cloud. Cada release nueva de estos componentes traen un montón de nuevas funcionalidades que nos hace la vida mas fácil a la hora de pensar en soluciones Cloud. Kudos para los developers de SpringFramework.

Libros varios

Un hobby que había dejado totalmente en el olvido era el de leer. Tenía mi Kindle juntando polvo en algún rincón hasta que me puse la meta de leer al menos 1 libro al mes (empezando como en septiembre) y puedo decir que me fue bastante bien. Les dejo un listado de libros interesantes que pude leer o terminar de leer durante el año pasado:

Cabe destacar todos estos libros son de escritores Chilenos y que por cierto disfruté cada uno en su ámbito. El libro de Patricio Bañados es como estar tras bambalinas de TVN en los tiempos donde nuestro país paso por un periodo triste de su historia. Pero lo mas interesante es cuando superamos el periodo de la dictadura y las historias de codazos, volteretas y bajezas políticas se hacen presente en la vida del Relator.

Los libros de Ciencias de José Maza, Gabriel León y María Teresa Ruíz todos tienen la característica de un lenguaje muy simple y de fácil comprensión. Todos ellos muy entretenidos. Ciencia Pop tiene muchas anécdotas científicas y descubrimientos que se han hecho de casualidad (como la mayoría de los descubrimientos científicos). Somos polvo de estrellas y Hijos de las estrellas nos invitan a reflexionar sobre el origen del universo, sobre el origen de la vida y de qué estamos hechos, literalmente… Polvo de estrellas.

Sobre la Naturaleza del Software, también es un libro de fácil lectura con muchas anécdotas del mundo informático. También invita a reflexionar sobre la evolución de la Ingeniería Informática, los errores recurrentes en esta área, de lo joven que es esta rama de la ingeniería y las comparaciones odiosas con otras ramas. Destaca a grandes personajes que han participado en la evolución del cómo se construye software hasta el día de hoy.

En proceso de lectura aún:

Sobre “Contacto” de Carl Sagan, es un libro lleno de detalles y abundante en información de los personajes… muy muy descriptivo (a veces aburre un poco). Lo que me chocó es que uno espera una cierta similitud con la película Contacto y dicha similitud no es tal. La película la habré visto al menos una docena de veces y siempre emociona el momento en que empieza a escuchar los pulsos que provienen de la estrella Vega o al final cuando les habla apasionadamente a los niños sobre el universo. El libro es totalmente diferente o mejor dicho… la película es “basada” en la novela original de Carl Sagan.

Lo mismo me pasó con “Yo, Robot”, la película es también una de mis favoritas y con el libro no pega ni junta, aunque la trama del libro es la misma que se trata en la película y tiene que ver con el conflicto que hay con las 3 leyes de la robótica, pero en muchas situaciones diferentes a la película. El libro contiene un conjunto historias cortas que hablan de la forma en que los robots interactúan con los humanos en situaciones límite exigiendo al máximo las 3 leyes de la robótica. Interesante libro!

Alguna sugerencia de lectura para este año? déjenlo en los comentarios.

Palabras al cierre

Feliz 2018 para todos! Bienvenido 2018!

y si… voy a escribir mas este año :D

Cambio de casa, adiós Octopress bienvenido Jekyll

Estoy de mudanza de blog, adiós Octopress bienvenido Jekyll.

Una de las razones que me empujó a cambiarme de sistema de blog, es que Octopress esta sacando la versión 3 desde el 15 de enero del 2015, esta algo abandonado (ver los commits).

La pregunta era… ¿ hacia donde migro ?

Octopress se basa en Jekyll y la organización de los artículos es básicamente la misma. Pero no todo es tan fácil. Octopress tiene un conjunto de plugins que facilitan y ayudan a embellecer los artículos, por ejemplo, para destacar código fuente, citas, imágenes y video.

Jekyll de alguna u otra forma también tiene plugins para lo mismo, el problema es que hay que modificar todos los artículos donde fueron usados los plugins de Octopress y modificarlos al gusto de Jekyll.

Siguiente problema, elegir un lindo (?) tema para el blog, después de un largo recorrido buscando, encontré Minimal Mistakes, bien lindo, personalizable, con gran capacidad de extender y todas esa funcionalidades que a los developers nos enamoran (pero que jamás usamos al 100%). De todas formas, la instalación de este tema no fue sencillo, demasiadas configuraciones y cosas que no andan a la primera y la documentación, que es bien buena, no dice las cosas clave.

Algunas características de Jekyll son:

  • Posee muchos plugins
  • Esta escrito en ruby (puaj)
  • Tiene varios mecanismos de importación desde otros blogs.
  • El contenido se genera a partir de documentos escritos en markdown
  • Como el contenido es texto, estos se pueden mantener fácilmente en git

Si ve errores, considere que el blog esta en rodaje y sea amable… reporte el bug.