Clone, run, and explore this sample
An interactive weather map backed by an Express + OpenTelemetry API that reads live data from the Open-Meteo external service, all orchestrated by a TypeScript AppHost (apphost.mts).
The entry point that composes every resource and dependency in this sample's distributed application.
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
// Real weather comes from Open-Meteo — a free forecast API that needs no key.// Model it as an external service so the dashboard shows the dependency and polls// its health, then inject its base URL into the API for on-demand, server-side calls.const openMeteoUrl = "https://api.open-meteo.com";
const weather = await builder .addExternalService("open-meteo", openMeteoUrl) .withHttpHealthCheck({ statusCode: 200, path: "/v1/forecast?latitude=0&longitude=0¤t=temperature_2m", });
// Run the Express API and expose its HTTP endpoint externally.// The API is written in TypeScript; load the tsx runtime so Node can execute// `.ts` files directly (native type stripping is only unflagged in Node >= 22.18).const api = await builder .addNodeApp("api", "./api", "src/index.ts") .withEnvironment("NODE_OPTIONS", "--import tsx") .withReference(weather) .withEnvironment("OPEN_METEO_URL", openMeteoUrl) .withHttpEndpoint({ env: "PORT" }) .withExternalHttpEndpoints() .withUrlForEndpoint("http", async (url) => { url.displayText = "API"; });
// Run the Vite frontend after the API and inject the API URL for local proxying.const frontend = await builder .addViteApp("frontend", "./frontend") .withReference(api) .waitFor(api);
// Bundle the frontend build output into the API container for publish/deploy.await api.publishWithContainerFiles(frontend, "./static");
await builder.build().run();An interactive weather map backed by an Express + OpenTelemetry API that reads live data from the Open-Meteo external service, all orchestrated by a TypeScript AppHost (apphost.mts).
The sample consists of two apps:
api: A TypeScript Express API run directly with tsx. It returns on-demand forecasts and place names by calling Open-Meteo (modeled as an Aspire external service) plus keyless reverse/forward geocoding, and serves an interactive Scalar API reference. OpenTelemetry instrumentation streams traces, metrics, and logs to the Aspire dashboard.
frontend: A React 19 + Vite app rendering a full-screen Leaflet map. Click or search any point to load real current conditions, an hourly strip, and a 5-day forecast, with an animated RainViewer radar overlay, switchable basemaps, geolocation, and a °F/°C toggle.
Architecture
Section titled ArchitectureRun mode:
flowchart LR
Browser -->|app + HMR| Vite[Vite dev server<br/>React + Leaflet]
Browser -->|/api/*| Vite
Vite -. proxy .-> Express[Express API<br/>tsx + OpenTelemetry]
Express --> OpenMeteo[Open-Meteo<br/>external service]
Browser -. map & radar tiles .-> Tiles[OSM / CARTO / Esri<br/>RainViewer]Publish mode:
flowchart LR
Browser -->|SPA + /api/*| Express[Express API<br/>serves ./static build output + API]
Express --> OpenMeteo[Open-Meteo<br/>external service]
Browser -. map & radar tiles .-> Tiles[OSM / CARTO / Esri<br/>RainViewer]What this demonstrates
Section titled What this demonstratesaddExternalService + withHttpHealthCheck: model the third-party Open-Meteo API as a first-class Aspire resource with a health check surfaced on the dashboard.
addNodeApp: run the TypeScript Express API directly with
tsx(NODE_OPTIONS=--import tsx).addViteApp: run the React + Vite frontend with hot module replacement.
withReference + waitFor: inject the Open-Meteo URL into the API and the API URL into the frontend, and order startup so the frontend waits for the API.
withHttpEndpoint / withExternalHttpEndpoints / withUrlForEndpoint: configure and label the endpoints shown in the dashboard.
publishWithContainerFiles: bundle the Vite build output into the API container so a single container serves both the SPA and the API in publish mode.
OpenTelemetry: distributed traces, metrics, and structured logs from the Node.js API, viewable in the dashboard.
Prerequisites
Section titled PrerequisitesNode.js - version 20.19+, 22.13+, or 24+ (see the
enginesfield inpackage.json)
Running the app
Section titled Running the appIf using the Aspire CLI, run aspire run from this directory.
If using VS Code, open this directory as a workspace and start the AppHost with the Aspire extension.
The dashboard links to the frontend. Click any point on the map, or search for a place, to load a live forecast for that location.
API endpoints
Section titled API endpointsThe api app exposes:
GET /api/weather?lat={lat}&lon={lon}- current conditions, hourly, and 5-day forecast for a coordinate.GET /api/geocode?q={query}- forward geocoding for the search box.GET /health- health probe used by the AppHost.GET /openapi.json- the raw OpenAPI 3.1 document.GET /reference- an interactive Scalar API reference.
Security notes
Section titled Security notesThis sample keeps the API endpoints public and unauthenticated so it is easy to run and inspect locally. It does not add authentication, authorization, CSRF protection, or rate limiting, so treat it as a demo pattern rather than production-ready API security.
Inputs are validated at the boundary (latitude/longitude ranges and query lengths) and every outbound call uses a bounded timeout, but the sample calls third-party services (Open-Meteo, geocoding, and RainViewer tiles) without a caching or quota strategy. Production services should add real authN/authZ, request rate limiting, bounded request bodies, response caching, and resilient handling of upstream outages.
For production applications, see the Node.js security best practices, Express security best practices, and the OWASP API Security Top 10.
Attributions
Section titled AttributionsThis sample relies on several free, community-maintained data sources. If you build on it, please honor their attribution and licensing requirements:
Weather data by Open-Meteo — current conditions and forecasts are provided by the Open-Meteo API, licensed under CC BY 4.0.
Map data © OpenStreetMap contributors — the base map geometry comes from the OpenStreetMap project and is available under the Open Database License (ODbL).
Radar imagery by RainViewer.
Reverse geocoding by BigDataCloud.
Sample screenshots
Select the image to zoom in.
