| app | ||
| assets | ||
| internal | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
NATS APP
Lectura de datos de sensores en un dispositivo IoT. Prueba técnica para optar por el puesto de programador Go.
TL:DR
- make lazy-start
- abre terminal y escribe:
nats sub sensors.data.*
Requisitos previos
- Docker
- NATS CLI
- Make, si prefieres la comodidad de usar Makefile
Comandos
Registrar un sensor
- Campos obligatorios:
sensor_idysensor_type. - Campos opcionales:
sampling,thresholdaboveythresholdbelow.
nats req sensors.register '{ "sensor_id": "sensor-001", "sensor_type": "temperature", "sampling": 3600, "thresoldabove": 50.0, "thresoldbelow": -10.0 }'
Actualizar configuración sensor
- Campo obligatorio:
sensor_idy la presencia de al menos un parámetro.
nats req sensors.update '{ "sensor_id": "sensor-001", "sensor_type": "temperature", "sampling": 10, "thresoldabove": 100.0, "thresoldbelow": -15.0 }'
Obtener información de un sensor
- Campo obligatorio:
sensor_id.
nats req sensors.get '{ "sensor_id": "sensor-001" }'
Obtener valores de un sensor
- Campo obligatorio:
sensor_id. - Campos opcionales:
fromytoen formato RFC3339. Si no se especifican, se toman los últimos 7 días.
nats req sensors.values.get '{ "sensor_id": "sensor-001", "from": "2025-10-03T00:00:00Z", "to": "2025-10-10T23:59:59Z" }'
Obtener listado de sensores
No hay payload, pero hay que poner comillas dobles o si no se queda esperando una entrada de datos.
nats req sensors.list ""
Suscribirse a un sensor
nats sub sensors.data.sensor-001
Suscribirse a todos los sensores
nats sub sensors.data.*
Consideraciones
Hay partes de códigos que son snippets extraídos de una librería de autoría propia. Repositorio GitHub. De las cuales son:
- El logger usando la stdlib log/slog.
- La conexión con la base de datos, usando el controlador pgx.
Bitácora
Quickstart y toma de contacto con NATS
Lo primero que he hecho es un quickstart del proyecto, con lo que siempre o casi siempre pongo en mis experimentos. Y lo siguiente, en lugar de empezar a construir el proyecto como loco he tratado de entender cómo funciona NATS. Al final ha sido muy sencillo, siguiendo esos pasos:
- Levantar el servidor NATS en Docker
- Instalar el CLI de NATS
- Abrir un puñado de terminales, y en un par de ellas escribir:
nats sub "hello"lo cual significa que se está suscribiendo al canalhello. Y en otra escribir:nats pub "hello" "Hola mundo!", lo cual significa que está escribiendo el mensajeHola mundo!en el canalhello.
Creación de la aplicación
La parte fácil es hacer todo el boilerplate, hecho en solo commit, que no es
lo ideal pero lo dicho, es puro relleno, mucho código ya viene escrito de la
librería gopher-toolbox. En este punto el proyecto compila pero hay error en
tiempo de ejecución por la falta de implementaciones en el repositorio.
Empezando por el repositorio
El almacenamiento de los datos se ha optado por el uso de TimescaleDB, una
extensión de PostgreSQL, el motivo es porque ya he trabajado mucho con ese
motor y tengo los drivers ya escritos. Tengo entendido que está optimizado
para trabajar con grandes cantidades de datos de series temporales, lo que viene
siendo valores de sensores por ejemplo.
Por otro lado también hay un sistema de caché muy rudimentario, en memoria que es un mapa de valores.
Para el registro de valores y mantener ambos se ha usado el patrón decorador que bajo un mismo struct se incluye las dos implementaciones y registra cambios en ambas partes. Desde la capa servicios sólo tiene que llamar al decorador sin saber los detalles de la implementación.
Continuamos con los servicios
Con el repositorio sin implementar, se puede realizar los servicios. En ese proyecto ha quedado muy básico, sirviendo solamente de enlace entre los controladores y el repositorio.
Cuando se creó el broker de NATS, inicialmente fue para crear una interfaz de mensajería donde se pudiese manejar websockets, SSE y otras mensajerías pero se consideró que había otras prioridades. Así se quedó.
Y finalmente los controladores
Ahí tuve muchas dudas con el entendimiento de NATS, estaba muy arraigado en el patrón REST, y cambiar la mentalidad costó un poco, al final mirando un poco la documentación me quedé con los conceptos clave:
- Está basado en asuntos, los canales se crean de forma jerárquica.
- Cuando se hace un subscribe, se suma a un canal del asunto dado.
- Para escribir en el canal, hay que hacer el publish.
- Finalmente para solicitar un recurso, está el request.
Esto es todo, entonces los controladores de la entidad sensors están constituidos por una serie de endpoints haciendo las acciones que se solicita.
El simulador
Basada en gorutinas y canales, cuando se inicia el simulador, se crea un canal para detener simuladores que están en ejecución para su actualización o detención.
Cuando se registra un nuevo sensor, está la función SimulateSensor, que se
inicia como una gorutina y usa el SamplingInterval para el canal ticker,
así llamar a generateData cada vez que toque.
Una vez que el dato está generado se hace una publicación al asunto sensor.data, que al mismo tiempo, el handler registerData lo captura al estar registrado al mismo asunto sensor.data.
Pruebas
La realización de pruebas unitarias de lo que son los controladores de NATS me han sido imposible hacerlas en condiciones, podría haber usado Claude pero es que no daba pie con bola y no entendía nada, así que por la máxima transparencia he optado por no incorporarlas.
Las pruebas más interesantes son las de reglas de negocio y validación, lo que viene a ser los servicios y dominio.
Generadores y otras librerías
Existen generadores de código para Golang, de hecho, se fomenta su desarrollo, hay un artículo interesante de Rob Pike hablando sobre ello. Muchas de las herramientas son muy interesantes usarlas ya que acelera mucho la generación de código repetitivo. Da mas confianza usar esas herramientas que la IA.
Para las consultas SQL con seguridad de tipos, existe la herramienta sqlc.
Para ese proyecto sólo se ha usado GoMock,
mantenida por Uber y sirve para usar la interfaz Repository sin usar una
base de datos real.
Por otro lado, hubiese sido interesante incorporar ginkgo, es un marco de trabajo de pruebas unitarias usando un lenguaje de dominio específico (DSL).
En lugar de escribir una prueba así:
Test_Validate(t *testing.T) { ... }se puede hacer de la siguiente manera:var _ Describe("Models", func () { ... }), y dentro del cuerpo se describen las pruebas a realizar.
No se ha incorporado porque hay que instalar la herramienta que ejecutan las pruebas, y no quería correr el riesgo de que no funcionase en otro equipo o no diesen los resultados esperados. Que se podría haber usado un contenedor Docker, sí, pero la prueba no consiste en eso.
También se ha planteado incorporar la librería testify, descartado porque para comprobar si existe el error y algunas comparaciones no era necesario meter una dependencia más.
Diagrama
El diagrama explica básicamente la estrucutra del proyecto en términos generales, se demuestra que el dominio sensors solo se comunica al exterior mediante el NATS y el logger.
Amarillo: exterior. Morado: infraestructura. Verde: dominio Blanco: simulador, los handlers está conectado sólo para llamar a la gorutina, pero en la realidad debería ir independiente.
Conclusión y cierre
Interesante reto donde realmente lo que más me ha costado son la realización de pruebas unitarias, hay veces que me cuesta coger el concepto. Tengo la teoría muy clara, pero a la hora de la verdad se me complica un poco las cosas. Además que mockear el sistema de mensajería debe tener su especial complejidad. También ha sido algo complejo entender la concurrencia y los canales, no es algo que haya trabajado en profundidad pero sí que se le ha puesto bastante empeño, cariño y nivel de detalle.
He puesto mucho en valor la arquitectura limpia con un toque personal, no es DDD puro ya que hay elementos que no deberían estar en dominio, pero en el proyecto que estoy trabajando ahora mismo se está diseñando de la misma manera y está funcionando muy bien.
También se ha evitado todo lo posible el uso de LLMs para la generación de código, y su uso ha sido para la toma de decisiones arquitectónicas, discusión y lectura rápida sobre los distintos funcionamientos de algunas librerías. En más de una ocasión he cuestionado las respuestas que da, teniendo que verificar con la documentación oficial. Pongo en valor mi capacidad para aprovechar la IA de la mejor forma posible, verificando la información, además recalco que justo el proyecto actual es una migración de un código en PHP completamente hecho con IA, y se puede ver patrones y errores comunes que comete.
Soy consciente de que hay margen de mejora, por ejemplo con los tests o con la documentación, se ha puesto especial esfuerzo y atención a que los nombres de las funciones, variables, métodos, estructuras y paquetes sean lo más autodescriptivos posibles. Se han puesto algunos comentarios. También hay esfuerzo por permitir ejecutar el proyecto por primera vez con la mínima intervención.
Un problema interesante que tuve que resolver, que como el sensor puede mandar
un valor ausente, el tipo float64 al hacer el unmarshal se establece a 0.0,
con lo que se puede considerar válido, con lo cual su solución fue el uso de
puntero, si se descubre que es nil se considera no válido.
Digamos que este proyecto resuelve el problema que se propone, un sistema que permite registrar y actualziar un sensor. Se puede ver su estado y los datos que se recogen (simulados) se guardan en una base de datos.
Espero que el proyecto sea de vuestro agrado y podamos tener una siguiente reunión.

