Free Data as the Backbone, Paid API Only for the Peak
Open-Meteo covers most of the forecast for free. Paid OpenWeather is only touched for a few instant values — and if it drops, the app keeps running.
A Free Backbone: Open-Meteo
When developing weather applications, you will sooner or later hit the financial and performance limits of data sources. Providers of highly accurate meteorological data charge a premium for their APIs. As your user base grows, bills for thousands of requests can quickly wipe out any economic viability of the project. Fortunately, modern software architecture allows us to solve this problem using a hybrid approach. The right combination of Open-Meteo and OpenWeather offers a balance between zero or low operational costs and high accuracy where it matters most to the user.
In this design, Open-Meteo serves as the free backbone of the entire system. This API stands out for its excellent availability, does not require complex API key management in its basic tier, and offers robust global datasets based on open physical models such as GFS or ECMWF. The complete forecast skeleton is fetched from Open-Meteo, including the current conditions, a detailed hourly forecast, and a seven-day daily outlook. For the vast majority of the displayed information, this source is entirely sufficient and provides a stable foundation for the end user, covering the bulk of their information needs.
Paid Overlay for Instant Values Only
Although global open models are highly accurate for trends and longer-term outlooks, they can lag slightly behind real-time conditions on the ground (nowcasting). This is where the paid OpenWeather API comes in. It boasts a dense network of local weather stations and radars, allowing it to provide highly accurate, real-time values for any given minute.
Instead of fetching the complete weekly forecast from the paid API—which would generate unsustainable costs under high traffic—we use OpenWeather strictly as a thin overlay. We query the paid source exclusively for the current conditions of the given location.
In practice, this means the system first builds the complete data structure from the free Open-Meteo API. It then takes a few selected real-time values from OpenWeather (such as the precise current temperature, feels-like temperature, and current weather condition code) and overwrites the corresponding fields in the original object. The rest of the forecast, including the data-heavy hourly and daily arrays, remains untouched. The result is high accuracy for current weather details while keeping operational costs to a minimum.
Best-Effort: Failures Are Silently Omitted
One of the fundamental rules of robust backend architecture is the assumption that any external service can fail at any time. A paid API might hit its rate limits, network outages can occur, or the provider might experience temporary latency spikes. However, the user should not notice such failures at all.
Therefore, all supplementary data layers—whether it is the paid OpenWeather overlay, the Air Quality Index (AQI), or official weather alerts—are designed on a best-effort basis. If a call to these supplementary services fails, the application must not return an error state or display a blank screen to the user.
The backend silently catches these errors, logs them for internal monitoring, and proceeds to assemble the response without them. In this scenario, the client receives a complete and fully valid forecast generated entirely from the backbone Open-Meteo API. The user experience is preserved, the application keeps running, and the system demonstrates high resilience against third-party outages.
Paid Calls Only in the Refresh Path
To prevent unexpected financial liabilities, it is crucial to strictly separate the data read path from the update path. In more complex systems built on a stack like NestJS, MongoDB, and Redis, there is a risk that a careless developer might call an expensive external API directly from a controller handling standard user read requests. During a sudden traffic spike, such a design could instantly deplete your budget.
Therefore, paid calls are injected exclusively into the server's refresh path. The standard read endpoint queried by the client application has no access to the paid service's API client at all. It only reads pre-warmed data from the database or cache.
Data updates occur in a controlled manner in the background, or via a specific refresh mechanism protected by strict rate-limiting. This ensures that the expensive API is called only when absolutely necessary, with a maximum of one paid API call per location within a defined time window. Consequently, the standard read controller can never accidentally generate unnecessary costs.
The Mapper as the Sole Point of Normalization
Different weather data providers use different response formats. While Open-Meteo returns data in flat arrays where indices represent time series, OpenWeather relies on deeply nested JSON objects. If these formats were mixed throughout the application, the codebase would quickly become unmaintainable.
The solution is strict data normalization at the system's entry point. A dedicated mapping layer serves this purpose. This mapper is the only place in the entire application that knows the specifics of each external API. It receives raw data from both sources, validates and transforms it, and merges it into a single shared Data Transfer Object (DTO).
The following example illustrates a simplified concept of such a mapper in TypeScript:
interface WeatherDTO {
temperature: number;
windSpeed: number;
weatherCode: number;
hourlyForecast: { time: string; temp: number }[];
}
interface OpenMeteoRaw {
current: { temperature_2m: number; wind_speed_10m: number; weather_code: number };
hourly: { time: string[]; temperature_2m: number[] };
}
interface OpenWeatherRaw {
main: { temp: number };
wind: { speed: number };
weather: { id: number }[];
}
export class WeatherMapper {
public static mapToDto(
openMeteoRaw: OpenMeteoRaw,
openWeatherRaw?: OpenWeatherRaw,
): WeatherDTO {
// Base skeleton from free Open-Meteo
const dto: WeatherDTO = {
temperature: openMeteoRaw.current.temperature_2m,
windSpeed: openMeteoRaw.current.wind_speed_10m,
weatherCode: openMeteoRaw.current.weather_code,
hourlyForecast: openMeteoRaw.hourly.time.map((time: string, index: number) => ({
time,
temp: openMeteoRaw.hourly.temperature_2m[index]
}))
};
// If a paid overlay is available, overwrite selected values
if (openWeatherRaw && openWeatherRaw.main) {
dto.temperature = openWeatherRaw.main.temp;
dto.windSpeed = openWeatherRaw.wind.speed;
dto.weatherCode = this.mapOpenWeatherCode(openWeatherRaw.weather[0].id);
}
return dto;
}
private static mapOpenWeatherCode(code: number): number {
// Internal conversion logic of weather codes to a unified standard
return code;
}
}
The rest of the application, including the frontend and database models, works exclusively with this unified DTO. If you decide to swap OpenWeather for another paid provider in the future, the change will only affect this single file. The architecture remains clean and easily extensible.
This robust approach to data integration and normalization was successfully deployed within the platform detailed in the entire Weather InTouch project. Combining a reliable free foundation with a precise paid overlay has proven in practice to be the best way to build a scalable and economically sustainable service that is resilient to third-party provider outages.

