Rain or Shine LLM Poetry App
If you'd like to get straight to the code, go here: https://github.com/ndsu-patmcmullen/csci-717-project-weather-llm-poetry
What if your morning weather update didn't just tell you it was 42°F and raining, but instead gave you a kid friendly poem?
That is the core idea behind Rain or Shine LLM Poetry, a web application I built to connect real-time weather forecasts to generative AI, delivering custom-crafted poems based on exactly what’s happening outside your window.
Whether you are a developer looking to build your first AI-integrated project or someone who just loves a good mix of art and science, this breakdown shows how the app works, the engineering pivots along the way, and how to build LLM apps in a fun way.
The Concept: Art Meets Atmosphere
At its heart, the app is simple: you type in your ZIP code, and you get a formatted, kid-friendly poem reflecting your local climate.
But behind that simple interface is a dynamic system that translates real-world physical states into creative written prompts.
The visual above highlights how entirely different a scene feels based on just three factors: time of day, weather conditions, and present elements. The backend code grabs these variables programmatically and feeds them to the LLM to anchor the poem's mood.
The Pivot: When AI Needs a Reality Check
When I first drew up the blueprint for this app, I planned to let the Gemini LLM handle everything—including fetching the local weather.
But I quickly ran into a classic hurdle in LLM development: the real-time data barrier. While Large Language Models are great creative engines, they cannot reliably query live local weather data on their own without external tools.
To solve this, I split the responsibilities. Instead of asking the AI what the weather is, it fetches the concrete data first, structures it, and then hands it to the AI to write the poem.
To make this happen, I orchestrated three different APIs:
| API | Inputs | Outputs | Core Purpose |
|---|---|---|---|
| Geocoding API | ZIP Code | Latitude & Longitude | Pinpoints the physical location coordinates. |
| Open-Meteo API | Latitude & Longitude | Current Temp, Wind, Sky Conditions | Retrieves highly accurate, real-time weather data. |
| Gemini API | Dynamically Compiled Prompt | Custom Poem | Generates the creative output based on the weather data. |
How It Works: The Data Pipeline
-
Geocoding Lookup: ZIP to Coordinates. The user inputs a ZIP code. The React frontend sends this to the backend Node service, which queries the Geocoding API to translate it into a exact latitude and longitude.
-
Weather Retrieval: Real-Time Data Fetch. Using those coordinates, the app pings Open-Meteo to capture current conditions.
-
Prompt Compilation: Variables to Text. The backend runs the weather data through a custom
PromptGeneratormodule. It dynamically constructs a prompt containing the city, time of day, temperature, and sky conditions. -
LLM Generation: Gemini Synthesis. The compiled prompt is sent to the Gemini API, which processes the context and instantly drafts a customized poem.
-
Caching & Response: Speed Optimization. The poem is saved to the backend's in-memory cache to prevent duplicate API requests, and then rendered on screen.
Software Construction: Building It Clean
A cool concept is only as good as the code supporting it. For this project, I focused heavily on core software engineering principles to ensure the application was modular, reliable, and production-ready:
1. Test-First Development (TDD)
Because the prompt generation relies on strict variable placement (making sure city names, temperatures, and times of day map correctly), I adopted a test-first approach for the PromptGenerator module. By writing unit tests with mock data before writing the code, I guaranteed the prompt structure stayed consistent and error-free. The backend maintains 100% unit test coverage using mocks for the external APIs.
2. Modularity & Dependency Injection
I split the codebase into single-responsibility services:
GeminiLlmServiceonly knows how to talk to Gemini.WeatherServiceonly knows how to query conditions.GeocodingServicestrictly handles ZIP conversions.
By using dependency injection, these services are cleanly decoupled. This made writing unit tests incredibly easy, as I could mock out the APIs without changing a single line of business logic.
3. Immutability & Caching
Data objects like the WeatherPoem type are designed to be completely immutable. Once a poem is generated, its state cannot be altered, reducing accidental side-effects as data flows between the backend services and the frontend UI. Coupled with an in-memory cache, this ensures fast, repeatable performance.
The Takeaway
"Rain or Shine" was a fantastic exercise in balancing creative AI capabilities with proper software engineering. By treating prompt construction like any other critical software module, complete with testing, immutability, and modular design, you can build LLM applications that are not only fun to play with but robust and highly maintainable.