You want to plan a trip to Goa. You have five days, a budget of 25,000 rupees, and a clear preference for beaches and seafood. Normally, this means opening ten browser tabs, reading outdated forum posts, and manually cobbling together an itinerary. Instead, imagine sending a single POST request and getting back a structured day-by-day plan with meal suggestions, activity lists, and an exact budget split. That is what this project delivers.

We will build a REST API using Spring Boot and Azure OpenAI. The API accepts a destination, budget, duration, and interests. It returns clean JSON that a frontend or mobile app can render immediately. No scraping. no hardcoded itineraries. Just an AI model prompted to act as a travel planner.

What the API Returns

The response is not a block of Markdown text you have to regex apart. It is a structured JSON object containing daily activities, meal recommendations, and a budget breakdown. For a Goa trip, you might receive a day-one segment that allocates 500 rupees for breakfast at a beach shack, a morning at Palolem, and an evening seafood dinner within a specific locality. Each day carries time slots, estimated costs, and tags like "beach" or "food." This structure matters because modern travel apps do not want to parse paragraphs. They want objects they can map to RecyclerViews or React components.

The Stack and Why It Fits

The project uses Spring Boot 3.5 with Spring AI. Spring AI is the critical piece. It provides a unified ChatModel abstraction so you do not have to write raw HTTP clients against Azure OpenAI. You swap dependencies and properties, not service code.

You need four dependencies in your build file:

  • spring-boot-starter-web for the REST layer.
  • spring-ai-starter-model-azure-openai to connect to the LLM through Spring AI’s interface.
  • springdoc-openapi for automatic Swagger documentation.
  • Lombok to cut down the boilerplate in your request and response POJOs.

Spring AI sits between your business logic and the LLM provider. That positioning is intentional. It keeps your @Service classes clean and provider-agnostic.

Prompt Engineering with PromptTemplates

Hardcoding prompts inside Java strings is a fast way to create unmaintainable software. If the product team decides the AI should sound more casual or refuse budget estimates above a certain threshold, you should not have to recompile your service.

Spring AI provides PromptTemplate. You store the prompt skeleton in a resource file or a dedicated template string, leaving placeholders for variables like {destination}, {budget}, {days}, and {interests}. At runtime, the service creates a Prompt object and injects the user’s values.

Separate system messages from user messages. Use the system message to define the persona. For example, you tell the model it is a travel planner specialized in Indian destinations, budget conscious, and strict about returning only JSON with no markdown fences. Use the user message to pass the specific trip details. This split helps when you later want to A/B test personas without changing the API contract.

The Service Layer: Talking to Azure OpenAI

The @Service class has one job. It builds the prompt, calls the model, cleans the response, and parses the result.

Inject Spring AI’s ChatClient or ChatModel. Render the PromptTemplate with the incoming request values, then call the chat method. The response arrives as a String. Here is where many tutorials stop and real production code starts.

LLMs sometimes add polite preambles. You might get a response that opens with "Here is your itinerary" and then dumps JSON wrapped in triple backticks. If you try to deserialize that directly with Jackson, your app crashes. Add a small helper method that scans the raw string, finds the first opening brace and the last closing brace, and extracts only the JSON payload. Then validate the extracted block. Check that required fields exist and that numeric values make sense before you return the object to the controller.

This defensive parsing is not optional. It is the boundary between a demo and a reliable API.

Handling Errors Like a Mature System

External APIs fail. Azure OpenAI will return rate limit errors, authentication failures, or transient 500s. If you let these bubble up to the user as stack traces, you lose credibility.

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.