Building a Weather Platform Whose Costs Track Locations, Not Users
Forecasts are a free commodity. The value is doing something proactive with them — without burning your paid-API budget. A case study of a full-stack TypeScript monorepo.

Why another weather app
Today, every major operating system provides users with free weather forecasts. Apple and Google have native widgets that are visually appealing, fast, and pre-installed on billions of devices. When a developer decides to build a new weather app, the first question they get from the community and investors isn’t “where will you get the data?” but “why would anyone download and open your app in the first place?” and “how do you plan to run it without API fees ruining you in the very first month?”
The weather app market is saturated with visually flawless products. Beautiful animated icons, smooth transitions, and detailed maps have become a commodity. However, users rarely want to spend time just looking at charts. The real value of a modern software service does not lie in passively showing the user that it is going to rain. Value is created when the app takes a proactive step — warning the user in time, adapting to their specific context, or automatically performing an action on their behalf. People do not pay to look at the weather; they pay to minimize risks, save time, or protect their property.
When designing Weather InTouch, I decided to build on two core pillars. The first is proactive data handling, where the app does not try to be just another passive dashboard, but an active assistant. The second pillar is strict operational cost optimization. The goal was to design an architecture where the number of paid calls scales with the number of tracked locations rather than with the number of users. This article describes how to build such an application from scratch.
A three-app monorepo and a single source of truth for data
Keeping the frontend, backend, and promotional website in sync is a constant source of frustration in many projects. Changing a single data field on the backend often leads to frontend compilation errors or breaks the API contract. For this reason, Weather InTouch is designed as a TypeScript monorepo built on npm workspaces. This approach allows sharing code, type definitions, and configuration files across the entire ecosystem with minimal overhead.
The entire monorepo consists of three main applications and one shared package:
- Frontend application: A client interface built on the Angular 22 platform. Angular was chosen for its robust architecture, strong support for dependency injection, and built-in state management tools, which simplify working with complex reactive forms and data streams.
- Backend API: A server-side application running on the NestJS 12 framework. NestJS provides a modular structure that perfectly complements Angular on the frontend and allows for easy integration with databases and external services.
- Static website: A custom TypeScript static-site generator (SSG) used to generate the marketing presentation and documentation. Using a custom, lightweight generator eliminates dependency on heavy CMS platforms and ensures maximum loading speed and optimal SEO without the need to run additional server infrastructure.
A single source of truth for data structures
At the heart of the monorepo is a shared DTO (Data Transfer Object) package. This package defines the exact shapes of all data flowing between the backend and frontend. Both the backend and frontend import identical TypeScript types and interfaces. If the structure of the weather forecast changes, the compiler immediately flags all locations in the client application where the code needs to be updated.
// packages/shared/src/dto/weather.dto.ts
export interface CoordinateDto {
latitude: number;
longitude: number;
}
export interface CurrentWeatherDto {
temperature: number;
windSpeed: number;
relativeHumidity: number;
weatherCode: number;
updatedAt: string;
}
export interface WeatherForecastResponseDto {
coordinates: CoordinateDto;
current: CurrentWeatherDto;
timezone: string;
}
This approach eliminates manual rewriting of data structures and reliance on outdated API documentation.
Architectural separation of concerns
From a security and performance perspective, where data is processed is strictly defined. The backend fully owns all access to paid APIs, handles user authorization, subscription management, and communication with the payment gateway. The frontend is designed to be as stateless as possible.
Components and visual cards on the frontend do not receive raw data from external APIs. Instead, they consume cleaned, transformed, and localized data ready for immediate rendering. This approach significantly simplifies testing the client application, as components can easily be tested using mock data that matches the shared DTO interfaces.
Free backbone, paid API behind a switch
A fundamental mistake when designing weather applications is immediately connecting all data streams to expensive commercial APIs. Providers like OpenWeather or Apple WeatherKit charge fees per thousand requests. If your application queries the server every time a widget is opened or a map is panned, infrastructure bills can quickly exceed generated revenue.
The Weather InTouch architecture addresses this issue with a hybrid data acquisition model. The free Open-Meteo API serves as the primary data backbone. It provides highly accurate data for current conditions, hourly forecasts, and daily forecasts up to 7 days ahead. Open-Meteo offers very favorable terms for both non-commercial and commercial use, and its data models are more than sufficient for most common scenarios.
The paid layer (OpenWeather in this case) is implemented on the backend behind a configurable feature flag, as an overlay refining a handful of live values. That flag is currently off on every route: the overlay overwrites values the free model already returns, at an order-of-magnitude higher cost per call. Production therefore runs on the free backbone alone, and the switch stays in the code as a documented option for when the overlay earns its invoice.
Graceful degradation
If the paid API exhausts its daily limit, encounters a network error, or is intentionally disabled to save costs, the system automatically switches to the fallback data source without user intervention. Although the user might lose a minor nuance in the accuracy of the current temperature, the application remains fully functional, providing a complete forecast from the primary free source.
The following example illustrates how the NestJS service handles this fallback behavior when the paid provider fails:
// apps/backend/src/modules/weather/weather.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { OpenMeteoService } from './open-meteo.service';
import { OpenWeatherService } from './open-weather.service';
import { WeatherForecastResponseDto } from '@weather-intouch/dtos';
@Injectable()
export class WeatherService {
private readonly logger = new Logger(WeatherService.name);
constructor(
private readonly openMeteo: OpenMeteoService,
private readonly openWeather: OpenWeatherService,
) {}
async getWeatherForecast(lat: number, lon: number): Promise<WeatherForecastResponseDto> {
const baseForecast = await this.openMeteo.fetchForecast(lat, lon);
try {
// Attempt to enrich data from the paid API if active
const refinedCurrent = await this.openWeather.fetchCurrentOverlay(lat, lon);
return {
...baseForecast,
current: {
...baseForecast.current,
temperature: refinedCurrent.temperature ?? baseForecast.current.temperature,
relativeHumidity: refinedCurrent.humidity ?? baseForecast.current.relativeHumidity,
updatedAt: new Date().toISOString(),
},
};
} catch (error) {
this.logger.warn(`Paid API failed or is unavailable. Using fallback data from Open-Meteo. Error: ${error.message}`);
return baseForecast;
}
}
}
This ensures that an outage of a single provider or a sudden API key restriction does not take the entire application down.
Cost-control engine as the backbone of the product
The real saving does not happen when you choose a provider, but in how we handle data within our infrastructure. If a thousand users in the same city open the app at the same moment, there is no reason to send a thousand requests to external weather servers. Weather does not change every second, and values do not differ over a distance of a few meters.
To reduce load and costs, a robust caching engine was developed, built on two key concepts: rounding coordinates to a spatial grid and the Single-Flight pattern. A more detailed look at this topic is available in the full project case study.
Spatial grid and cache sharing
When a mobile device transmits GPS coordinates, it sends them with an accuracy of six to eight decimal places. That is millimeter to centimeter precision. For weather forecasting purposes, however, such precision is counterproductive because it generates an infinite number of unique cache keys.
Weather InTouch therefore implements coordinate rounding to four decimal places. At Central European latitudes this corresponds to a grid roughly 11 meters tall and about 7 meters wide — meridians converge toward the pole, so it is never a square.
- The coordinates
50.087234, 14.421123are rounded to50.0872, 14.4211. - The coordinates of a neighboring user standing ten meters away round to the exact same values.
Thanks to this rounding, all users in a given microsegment share an identical cache entry. Two layers guard the upstream, and neither of them is a second cache:
- In-process single-flight: a map of in-flight promises inside the running NestJS instance, so concurrent requests for the same location wait on one call.
- Redis lock: the same mechanism across backend instances. The cache store itself is then either Redis or — when
REDIS_URLis absent — a single instance’s memory as a development fallback.
As a result, the upstream API is contacted at most once per location within a defined time interval (on the order of tens of minutes).
Cache stampede prevention using Single-Flight
Under high traffic, a cache stampede or dogpiling can occur. The moment a cache entry expires and dozens of concurrent requests for the same location hit the server, all of these requests find the cache empty and simultaneously attempt to query the external API. This leads to immediate rate-limit exhaustion and unnecessary costs.
The Single-Flight pattern (implemented in Node.js using a map of active Promises) ensures that only one active outgoing request to the external API can run for any unique location at any given time.
// apps/backend/src/modules/weather/single-flight.manager.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class SingleFlightManager {
private activeRequests = new Map<string, Promise<unknown>>();
async execute<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
const existingPromise = this.activeRequests.get(key);
if (existingPromise) {
// If a request for the same key is already running, return the existing Promise
return existingPromise as Promise<T>;
}
// Create a new request and store it in the map
const promise = fetchFn().finally(() => {
this.activeRequests.delete(key);
});
this.activeRequests.set(key, promise);
return promise;
}
}
Thus, if 150 concurrent requests for a Prague 1 forecast hit the server, only one physical query is made to the external API. The remaining 149 requests wait for this single query to complete and then receive the identical result.
Current capabilities
In its current version, Weather InTouch is a functional MVP under active development that already offers a comprehensive set of features. The application was designed from the ground up with modularity in mind, making it easy to add new data layers.
Key integrated features include:
- Current conditions and multi-day forecast: A standard overview of temperature, humidity, pressure, and wind speed, complemented by a six-day outlook on the card (the data reaches seven).
- Hourly multi-chart: An interactive visualization that allows users to track temperature, precipitation, and wind trends hour-by-hour on a single timeline.
- Precipitation radar: A map layer showing the movement of precipitation areas with the ability to play back recent frames.
- Air quality, pollen levels, and UV index: Critical data for allergy sufferers and outdoor enthusiasts, integrating the Air Quality Index (AQI) and concentrations of major pollen allergens.
- Official weather alerts: The system processes and displays official weather alerts in the CAP (Common Alerting Protocol) format issued by national hydrometeorological institutes.
- Location management: Users can save their favorite locations for quick access.
- Localization and units: Full support for 20 world languages and automatic switching between metric and imperial unit systems.
Security and user management
In the backend architecture (NestJS + MongoDB + Redis), strong emphasis is placed on user security and privacy.
- Authentication: Sign-in uses a Google account (token-on-backend validation). The server receives the authorization token from the device, validates it with the issuing authority, and issues its own secure, short-lived JWT token.
- Billing: Subscription and payment management is handled through Stripe. The system supports three access tiers: Free, Plus, and Pro.
- GDPR compliance: Every user can export, from their profile settings, all their stored data (saved locations, monitoring rules, settings) with a single click, or request immediate and complete account deletion from the MongoDB database without having to contact support.
What shipped since, and what is on the roadmap
The current version of Weather InTouch is a stable foundation to build on. Since the project’s goal is to move from passive data display to proactive assistance, planned features focus on automation and integration into the broader technological ecosystem.
Watchdog with user-defined thresholds — shipped
This feature lets users define their own rules for weather monitoring. Instead of a generic rain warning, a user sets specific conditions, such as: “Send me an alert if the wind speed in my location exceeds 15 m/s” or “Alert me a day in advance if the temperature drops below freezing so I can winterize my plants.”
Automation and webhooks — shipped
This connects weather forecasts with smart home setups and enterprise systems. Using outgoing webhooks, actions can be triggered automatically based on meteorological conditions; the receiving end is something the user wires up on their own side. If the system detects approaching heavy rain, it will send a webhook to the home automation system, which can then close skylights or postpone scheduled lawn irrigation.
Embeddable widget for third-party websites
An embeddable widget for third-party sites is on hold. If it came back, it would be a themable card wired to the same cache, so it would not weigh down the host site.
Multi-tenant layer and mobile applications
A multi-tenant interface is in development for enterprise customers, allowing weather tracking across hundreds of branches under a single administrator account. In parallel, a native mobile shell built on Capacitor already exists, with both iOS and Android projects; it has not been released to the stores yet. That shell is what will make full use of background push notifications.
If you are interested in the technical implementation details, database architecture, or how radar imagery is integrated, visit the full project case study for a detailed breakdown of the entire development cycle and infrastructure setup.

