Quieres planificar un viaje a Goa. Tienes cinco días, un presupuesto de 25.000 rupias y una clara preferencia por las playas y los mariscos. Normalmente, esto significa abrir diez pestañas en el navegador, leer publicaciones obsoletas en foros y armar un itinerario manualmente. En su lugar, imagina enviar una única solicitud POST y recibir a cambio un plan estructurado día a día con sugerencias de comidas, listas de actividades y un desglose exacto del presupuesto. Eso es lo que ofrece este proyecto.
Construiremos una API REST utilizando Spring Boot y Azure OpenAI. La API acepta un destino, un presupuesto, una duración y unos intereses. Devuelve un JSON limpio que una aplicación frontend o móvil puede renderizar inmediatamente. Sin scraping. Sin itinerarios predefinidos. Simplemente un modelo de IA con un prompt para actuar como planificador de viajes.
Qué devuelve la API
La respuesta no es un bloque de texto Markdown que tengas que separar mediante regex. Es un objeto JSON estructurado que contiene actividades diarias, recomendaciones de comidas y un desglose del presupuesto. Para un viaje a Goa, podrías recibir un segmento del primer día que asigne 500 rupias para el desayuno en un chiringuito de playa, una mañana en Palolem y una cena de mariscos por la noche en una localidad específica. Cada día incluye franjas horarias, costes estimados y etiquetas como "beach" o "food". Esta estructura es importante porque las aplicaciones de viajes modernas no quieren analizar párrafos; quieren objetos que puedan mapear a RecyclerViews o componentes de React.
El stack y por qué encaja
El proyecto utiliza Spring Boot 3.5 con Spring AI. Spring AI es la pieza crítica. Proporciona una abstracción unificada de ChatModel para que no tengas que escribir clientes HTTP puros para Azure OpenAI. Cambias dependencias y propiedades, no el código del servicio.
Necesitas cuatro dependencias en tu archivo de construcción:
spring-boot-starter-webpara la capa REST.spring-ai-starter-model-azure-openaipara conectarse al LLM a través de la interfaz de Spring AI.springdoc-openapipara la documentación automática de Swagger.Lombokpara reducir el código repetitivo (boilerplate) en tus POJOs de solicitud y respuesta.
Spring AI se sitúa entre tu lógica de negocio y el proveedor del LLM. Ese posicionamiento es intencional. Mantiene tus clases @Service limpias e independientes del proveedor.
Ingeniería de prompts con PromptTemplates
Escribir los prompts directamente en cadenas de Java es una forma rápida de crear software difícil de mantener. Si el equipo de producto decide que la IA debe sonar más informal o que debe rechazar estimaciones de presupuesto por encima de cierto umbral, no deberías tener que recompilar tu servicio.
Spring AI proporciona PromptTemplate. Almacenas el esqueleto del prompt en un archivo de recursos o en una cadena de plantilla dedicada, dejando marcadores de posición para variables como {destination}, {budget}, {days} e {interests}. En tiempo de ejecución, el servicio crea un objeto Prompt e inyecta los valores del usuario.
Separa los mensajes del sistema de los mensajes del usuario. Utiliza el mensaje del sistema para definir la personalidad (persona). Por ejemplo, le dices al modelo que es un planificador de viajes especializado en destinos de la India, consciente del presupuesto y estricto en devolver solo JSON sin delimitadores de markdown. Utiliza el mensaje del usuario para pasar los detalles específicos del viaje. Esta división ayuda cuando más adelante quieras realizar pruebas A/B de las personalidades sin cambiar el contrato de la API.
La capa de servicio: Comunicación con Azure OpenAI
La clase @Service tiene un solo trabajo. Construye el prompt, llama al modelo, limpia la respuesta y analiza el resultado.
Inyecta ChatClient o ChatModel de Spring AI. Renderiza el PromptTemplate con los valores de la solicitud entrante y luego llama al método de chat. La respuesta llega como un String. Aquí es donde muchos tutoriales se detienen y donde comienza el código de producción real.
Los LLM a veces añaden preámbulos de cortesía. Podrías recibir una respuesta que comience con "Aquí tienes tu itinerario" y luego vuelque un JSON envuelto en triples comillas invertidas. Si intentas deserializar eso directamente con Jackson, tu aplicación fallará. Añade un pequeño método auxiliar que escanee la cadena sin procesar, encuentre la primera llave de apertura y la última llave de cierre, y extraiga únicamente la carga útil (payload) JSON. Luego, valida el bloque extraído. Comprueba que existan los campos obligatorios y que los valores numéricos tengan sentido antes de devolver el objeto al controlador.
Este parseo defensivo no es opcional. Es la frontera entre una demo y una API fiable.
Gestión de errores como un sistema maduro
Las API externas fallan. Azure OpenAI devolverá errores de límite de tasa (rate limit), fallos de autenticación o errores 500 transitorios. Si permites que estos errores se propaguen hasta el usuario como trazas de la pila (stack traces), perderás credibilidad.
Use @RestControllerAdvice to intercept exceptions globally. Map Spring AI exceptions, HttpClientErrorException, and generic RuntimeExceptions to consistent error responses. Return a JSON body with a clear message, an HTTP status like 429 for rate limits, and enough detail for the client to retry or log the issue. The user should see something like "Service temporarily busy. Please retry in 30 seconds," not a screen full of Java class names.
Never Hardcode Secrets
Your Azure OpenAI API key does not belong in application.properties checked into Git. Externalize it. Use environment variables referenced in your Spring configuration, such as ${AZURE_OPENAI_KEY} and ${AZURE_OPENAI_ENDPOINT}. Keep a local .env file for development, add it to .gitignore, and load it through Spring Boot’s relaxed binding. If a key leaks, you rotate it in one place rather than rebuilding your artifact.
Testing Through Swagger
The springdoc-openapi dependency exposes a Swagger UI endpoint at runtime. Once your application starts, open /swagger-ui.html in a browser. You can fill in the Goa example directly: destination as "Goa," budget as 25000, days as 5, interests as "beaches, food." Hit execute and watch the JSON itinerary appear. This lets you validate prompt changes, verify serialization, and share a live playground with frontend developers before either side writes a unit test.
Swapping Providers Without Rewriting Code
Startups change providers. Maybe Azure credits expire, or you want to run inference against a local Ollama instance to cut costs. Because Spring AI abstracts the ChatModel interface, the swap is mechanical. Change the Maven dependency from spring-ai-starter-model-azure-openai to another starter, update your properties file with the new endpoint and key, and leave your service class alone. The API contract seen by your mobile app stays identical.
That portability makes this architecture particularly useful for real products. You are not marrying Azure. You are using it as one engine plugged into a clean Spring pipeline.
The Real Takeaway
An AI model is not your application. It is an external service that returns unpredictable text. Treat it with the same rigor you would give a payment gateway or a third-party weather API. Externalize your credentials. Validate every response. Clean the payload before parsing. Handle errors globally so your users never see a stack trace.
Let the AI handle the creative work of building a Goa itinerary on a 25,000-rupee budget. You handle the plumbing. When the two stay separate, you get a system that actually ships.
The original walkthrough that inspired this article can be found here.
Interested in discussing Spring AI and similar projects? Join the GyaanSetu learning community.
