// Get April climate for Tokyo
const response = await fetch(
"https://worlddataapi.com/v1/travel/JP?date=2026-04-15",
{ headers: { "X-API-Key": "YOUR_API_KEY" } },
);
const data = await response.json();
console.log(data.climate);
# Get April climate for Tokyo
import requests
response = requests.get(
"https://worlddataapi.com/v1/travel/JP?date=2026-04-15",
headers={"X-API-Key": "YOUR_API_KEY"}
)
data = response.json()
print(data["climate"])
# Get April climate for Tokyo
curl "https://worlddataapi.com/v1/travel/JP?date=2026-04-15" \
-H "X-API-Key: YOUR_API_KEY"
Example response:
{
"month": "April",
"temp_high_c": 19.2,
"temp_low_c": 10.4,
"precipitation_mm": 125.0,
"precipitation_hours": 10.2,
"sunshine_hours": 184.5,
"wind_speed_kmh": 15.3,
"humidity_percent": 62.0,
"cloud_cover_percent": 48.0
}
"When's the best time to visit?" is one of the most common travel questions. This guide covers building a feature that answers it with historical climate data—temperatures, rainfall, and sunshine hours that help travelers plan.
The Challenge#
Travelers need weather information to plan trips, but real-time forecasts only work for the next few days. For trips planned weeks or months ahead, historical climate averages are the answer. The challenge is presenting this data in a way that helps users make decisions: choosing the right month to visit, comparing destinations, and knowing what to pack.
Building this feature requires handling several complexities:
Fetching and caching monthly data for entire years
Creating meaningful visualizations of temperature and rainfall patterns
Generating actionable recommendations from raw numbers
Comparing multiple destinations fairly
Prerequisites#
Before starting, you need:
A World Data API key with access to the travel endpoint (Starter plan or higher)
Basic knowledge of JavaScript/React for the UI examples
Familiarity with async/await and fetch API
Important: The climate data provided by this API represents historical monthly averages, not real-time weather or forecasts. Use this data to show what is typical for a location, not what will definitely happen on specific dates.
Understanding Climate Data#
The API provides historical monthly averages, not forecasts:
| Field | Description | Use For |
|---|---|---|
temp_high_c | Average daily high | Daytime clothing, activities |
temp_low_c | Average daily low | Evening wear, heating needs |
precipitation_mm | Monthly rainfall | Rainy season detection |
precipitation_hours | Hours of precipitation | Frequency vs. intensity |
sunshine_hours | Hours of sunshine | Beach trips, photography |
humidity_percent | Average humidity | Comfort, hair/skin considerations |
wind_speed_kmh | Average wind speed | Outdoor activities, sailing |
cloud_cover_percent | Cloud coverage | Photography, sightseeing |
Important: This is climate data (historical averages), not weather forecasts. It tells you what is typical, not what will definitely happen. Actual conditions during a trip may differ significantly from these averages.
Fetching Climate for All Months#
To show a year-round overview:
async function getYearClimate(destination) {
const months = [];
// Fetch climate for each month
// Use a date in the middle of each month
for (let month = 1; month <= 12; month++) {
const date = `2026-${String(month).padStart(2, "0")}-15`;
const response = await fetch(
`https://worlddataapi.com/v1/travel/${destination}?date=${date}`,
{ headers: { "X-API-Key": API_KEY } },
);
const data = await response.json();
if (data.climate) {
months.push({
month: month,
name: data.climate.month,
...data.climate,
});
}
}
return months;
}
// Or fetch all in parallel for speed
async function getYearClimateParallel(destination) {
const monthDates = Array.from(
{ length: 12 },
(_, i) => `2026-${String(i + 1).padStart(2, "0")}-15`,
);
const responses = await Promise.all(
monthDates.map((date) =>
fetch(
`https://worlddataapi.com/v1/travel/${destination}?date=${date}`,
{ headers: { "X-API-Key": API_KEY } },
).then((r) => r.json()),
),
);
return responses
.filter((r) => r.climate)
.map((r, i) => ({
month: i + 1,
name: r.climate.month,
...r.climate,
}));
}
Building the Climate Overview UI#
function ClimateOverview({ destination }) {
const [climate, setClimate] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getYearClimateParallel(destination)
.then(setClimate)
.finally(() => setLoading(false));
}, [destination]);
if (loading) return <div>Loading climate data...</div>;
return (
<div className="climate-overview">
<h2>Climate in {getCountryName(destination)}</h2>
<div className="climate-chart">
<TemperatureChart data={climate} />
</div>
<div className="climate-months">
{climate.map((month) => (
<MonthCard key={month.month} data={month} />
))}
</div>
<BestTimeToVisit climate={climate} />
</div>
);
}
function MonthCard({ data }) {
return (
<div className="month-card">
<h3>{data.name}</h3>
<div className="temps">
<span className="high">{Math.round(data.temp_high_c)}°</span>
<span className="low">{Math.round(data.temp_low_c)}°</span>
</div>
<div className="details">
<div className="rain">
<span className="label">Rain:</span>
<span>{Math.round(data.precipitation_mm)}mm</span>
</div>
<div className="sun">
<span className="label">Sun:</span>
<span>{Math.round(data.sunshine_hours)}h</span>
</div>
</div>
</div>
);
}
Temperature Chart#
Visualize temperature ranges throughout the year:
function TemperatureChart({ data }) {
const maxTemp = Math.max(...data.map((d) => d.temp_high_c));
const minTemp = Math.min(...data.map((d) => d.temp_low_c));
const range = maxTemp - minTemp;
const chartHeight = 200;
const chartWidth = 100;
const scaleY = (temp) => {
return chartHeight - ((temp - minTemp) / range) * chartHeight;
};
return (
<div className="temperature-chart">
<svg
viewBox={`0 0 ${data.length * chartWidth} ${chartHeight + 40}`}
>
{/* High temperature line */}
<path
d={data
.map(
(d, i) =>
`${i === 0 ? "M" : "L"} ${i * chartWidth + chartWidth / 2} ${scaleY(d.temp_high_c)}`,
)
.join(" ")}
fill="none"
stroke="#ff6b6b"
strokeWidth="2"
/>
{/* Low temperature line */}
<path
d={data
.map(
(d, i) =>
`${i === 0 ? "M" : "L"} ${i * chartWidth + chartWidth / 2} ${scaleY(d.temp_low_c)}`,
)
.join(" ")}
fill="none"
stroke="#4dabf7"
strokeWidth="2"
/>
{/* Temperature range fill */}
<path
d={[
...data.map(
(d, i) =>
`${i === 0 ? "M" : "L"} ${i * chartWidth + chartWidth / 2} ${scaleY(d.temp_high_c)}`,
),
...data
.slice()
.reverse()
.map(
(d, i) =>
`L ${(data.length - 1 - i) * chartWidth + chartWidth / 2} ${scaleY(d.temp_low_c)}`,
),
"Z",
].join(" ")}
fill="url(#tempGradient)"
opacity="0.3"
/>
{/* Month labels */}
{data.map((d, i) => (
<text
key={d.month}
x={i * chartWidth + chartWidth / 2}
y={chartHeight + 20}
textAnchor="middle"
fontSize="12"
>
{d.name.substring(0, 3)}
</text>
))}
<defs>
<linearGradient
id="tempGradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor="#ff6b6b" />
<stop offset="100%" stopColor="#4dabf7" />
</linearGradient>
</defs>
</svg>
<div className="legend">
<span className="high">● High</span>
<span className="low">● Low</span>
</div>
</div>
);
}
Best Time to Visit Algorithm#
Analyze climate data to recommend visit times:
function analyzeBestTime(climate, preferences = {}) {
const {
preferWarm = true, // Prefer warmer temperatures
avoidRain = true, // Minimize rainfall
needSunshine = false, // Prioritize sunny days
idealTemp = 22, // Ideal temperature in °C
} = preferences;
// Score each month
const scored = climate.map((month) => {
let score = 0;
// Temperature score (closer to ideal = better)
const avgTemp = (month.temp_high_c + month.temp_low_c) / 2;
const tempDiff = Math.abs(avgTemp - idealTemp);
score += Math.max(0, 20 - tempDiff); // Max 20 points
// Rainfall score (less = better if avoiding rain)
if (avoidRain) {
const maxRain = Math.max(...climate.map((m) => m.precipitation_mm));
const rainScore = 20 * (1 - month.precipitation_mm / maxRain);
score += rainScore;
}
// Sunshine score
if (needSunshine) {
const maxSun = Math.max(...climate.map((m) => m.sunshine_hours));
const sunScore = 15 * (month.sunshine_hours / maxSun);
score += sunScore;
}
// Comfort penalty for extreme humidity
if (month.humidity_percent > 80) {
score -= 5;
}
return {
...month,
score: Math.round(score),
};
});
// Sort by score
const ranked = [...scored].sort((a, b) => b.score - a.score);
return {
best: ranked.slice(0, 3),
avoid: ranked.slice(-2),
all: scored,
};
}
function BestTimeToVisit({ climate }) {
const [preferences, setPreferences] = useState({});
const analysis = analyzeBestTime(climate, preferences);
return (
<div className="best-time">
<h3>Best Time to Visit</h3>
<div className="recommendations">
<div className="best">
<h4>Recommended Months</h4>
<ul>
{analysis.best.map((m) => (
<li key={m.month}>
<strong>{m.name}</strong>
<span className="details">
{Math.round(m.temp_high_c)}°/
{Math.round(m.temp_low_c)}°,
{Math.round(m.precipitation_mm)}mm rain
</span>
</li>
))}
</ul>
</div>
<div className="avoid">
<h4>Consider Avoiding</h4>
<ul>
{analysis.avoid.map((m) => (
<li key={m.month}>
<strong>{m.name}</strong>
<span className="reason">
{getAvoidReason(m, climate)}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
function getAvoidReason(month, climate) {
const maxRain = Math.max(...climate.map((m) => m.precipitation_mm));
const avgTemp = (month.temp_high_c + month.temp_low_c) / 2;
if (month.precipitation_mm > maxRain * 0.8) {
return "Rainy season";
}
if (avgTemp > 30) {
return "Very hot";
}
if (avgTemp < 5) {
return "Very cold";
}
if (month.humidity_percent > 85) {
return "High humidity";
}
return "Less ideal weather";
}
Packing Suggestions#
Generate packing recommendations based on climate:
function PackingSuggestions({ climate }) {
const avgHigh = climate.temp_high_c;
const avgLow = climate.temp_low_c;
const rainfall = climate.precipitation_mm;
const humidity = climate.humidity_percent;
const suggestions = [];
// Temperature-based clothing
if (avgHigh > 30) {
suggestions.push({
category: "Clothing",
items: [
"Light, breathable clothing",
"Shorts",
"T-shirts",
"Sun hat",
"Sunglasses",
],
});
} else if (avgHigh > 20) {
suggestions.push({
category: "Clothing",
items: [
"Light layers",
"Mix of short and long sleeves",
"Light jacket for evenings",
],
});
} else if (avgHigh > 10) {
suggestions.push({
category: "Clothing",
items: [
"Warm layers",
"Sweater or fleece",
"Light jacket",
"Long pants",
],
});
} else {
suggestions.push({
category: "Clothing",
items: [
"Warm coat",
"Thermal layers",
"Hat and gloves",
"Warm socks",
"Scarf",
],
});
}
// Rain gear
if (rainfall > 100) {
suggestions.push({
category: "Rain Gear",
items: [
"Waterproof jacket",
"Umbrella",
"Waterproof shoes or covers",
],
});
} else if (rainfall > 50) {
suggestions.push({
category: "Rain Gear",
items: ["Compact umbrella", "Light rain jacket"],
});
}
// Sun protection
if (avgHigh > 25 || climate.sunshine_hours > 200) {
suggestions.push({
category: "Sun Protection",
items: [
"Sunscreen SPF 30+",
"Sunglasses",
"Sun hat",
"Lip balm with SPF",
],
});
}
// Humidity considerations
if (humidity > 75) {
suggestions.push({
category: "High Humidity",
items: [
"Moisture-wicking fabrics",
"Anti-chafing products",
"Extra deodorant",
],
});
}
return (
<div className="packing-suggestions">
<h3>Packing Suggestions for {climate.month}</h3>
<p className="climate-summary">
Expect highs around {Math.round(avgHigh)}°C and lows around{" "}
{Math.round(avgLow)}°C.
{rainfall > 50
? ` About ${Math.round(rainfall)}mm of rain expected.`
: ""}
</p>
<div className="suggestion-categories">
{suggestions.map((cat) => (
<div key={cat.category} className="category">
<h4>{cat.category}</h4>
<ul>
{cat.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
))}
</div>
</div>
);
}
Comparing Destinations#
Help users compare multiple destinations:
function DestinationComparison({ destinations, travelMonth }) {
const [climateData, setClimateData] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const date = `2026-${String(travelMonth).padStart(2, "0")}-15`;
Promise.all(
destinations.map(async (dest) => {
const response = await fetch(
`https://worlddataapi.com/v1/travel/${dest}?date=${date}`,
{ headers: { "X-API-Key": API_KEY } },
);
const data = await response.json();
return { destination: dest, climate: data.climate };
}),
)
.then((results) => {
const data = {};
results.forEach((r) => {
data[r.destination] = r.climate;
});
setClimateData(data);
})
.finally(() => setLoading(false));
}, [destinations, travelMonth]);
if (loading) return <div>Comparing destinations...</div>;
return (
<div className="destination-comparison">
<h3>Climate Comparison for {getMonthName(travelMonth)}</h3>
<table>
<thead>
<tr>
<th>Destination</th>
<th>Temperature</th>
<th>Rain</th>
<th>Sunshine</th>
<th>Humidity</th>
</tr>
</thead>
<tbody>
{destinations.map((dest) => {
const climate = climateData[dest];
if (!climate) return null;
return (
<tr key={dest}>
<td>{getCountryName(dest)}</td>
<td>
<span className="high">
{Math.round(climate.temp_high_c)}°
</span>
/
<span className="low">
{Math.round(climate.temp_low_c)}°
</span>
</td>
<td>
{Math.round(climate.precipitation_mm)}mm
</td>
<td>{Math.round(climate.sunshine_hours)}h</td>
<td>{Math.round(climate.humidity_percent)}%</td>
</tr>
);
})}
</tbody>
</table>
<ComparisonHighlights data={climateData} />
</div>
);
}
function ComparisonHighlights({ data }) {
const destinations = Object.keys(data);
const warmest = destinations.reduce((a, b) =>
data[a].temp_high_c > data[b].temp_high_c ? a : b,
);
const driest = destinations.reduce((a, b) =>
data[a].precipitation_mm < data[b].precipitation_mm ? a : b,
);
const sunniest = destinations.reduce((a, b) =>
data[a].sunshine_hours > data[b].sunshine_hours ? a : b,
);
return (
<div className="highlights">
<div className="highlight">
<span className="label">Warmest:</span>
<span>
<strong>{getCountryName(warmest)}</strong>
</span>
</div>
<div className="highlight">
<span className="label">Driest:</span>
<span>
<strong>{getCountryName(driest)}</strong>
</span>
</div>
<div className="highlight">
<span className="label">Sunniest:</span>
<span>
<strong>{getCountryName(sunniest)}</strong>
</span>
</div>
</div>
);
}
Seasonal Activities#
Suggest activities based on weather conditions:
function suggestActivities(climate) {
const activities = [];
const { temp_high_c, precipitation_mm, sunshine_hours, wind_speed_kmh } =
climate;
// Beach activities
if (temp_high_c > 25 && precipitation_mm < 100 && sunshine_hours > 150) {
activities.push({
type: "beach",
name: "Beach & Swimming",
reason: "Warm temperatures and sunny skies",
});
}
// Hiking
if (temp_high_c > 10 && temp_high_c < 30 && precipitation_mm < 150) {
activities.push({
type: "hiking",
name: "Hiking & Trekking",
reason: "Comfortable temperatures for outdoor activities",
});
}
// City sightseeing
if (temp_high_c > 5 && temp_high_c < 28 && precipitation_mm < 200) {
activities.push({
type: "sightseeing",
name: "City Sightseeing",
reason: "Pleasant walking weather",
});
}
// Photography
if (sunshine_hours > 150 && climate.cloud_cover_percent < 60) {
activities.push({
type: "photography",
name: "Photography",
reason: "Good lighting conditions",
});
}
// Water sports
if (temp_high_c > 20 && wind_speed_kmh > 10 && wind_speed_kmh < 30) {
activities.push({
type: "watersports",
name: "Water Sports",
reason: "Good wind conditions",
});
}
// Winter sports
if (temp_high_c < 5) {
activities.push({
type: "winter",
name: "Winter Sports",
reason: "Cold enough for snow activities",
});
}
// Indoor activities (bad weather alternative)
if (precipitation_mm > 150 || temp_high_c < 0 || temp_high_c > 35) {
activities.push({
type: "indoor",
name: "Museums & Indoor",
reason: "Great backup for challenging weather",
});
}
return activities;
}
Caching Climate Data#
Climate data is historical and stable. Cache for long periods:
const CLIMATE_CACHE_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days
class ClimateCache {
constructor() {
this.cache = new Map();
}
getCacheKey(destination, month) {
return `climate:${destination}:${month}`;
}
async get(destination, month) {
const key = this.getCacheKey(destination, month);
// Check memory cache
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < CLIMATE_CACHE_TTL) {
return cached.data;
}
// Check localStorage
try {
const stored = localStorage.getItem(key);
if (stored) {
const { data, timestamp } = JSON.parse(stored);
if (Date.now() - timestamp < CLIMATE_CACHE_TTL) {
this.cache.set(key, { data, timestamp });
return data;
}
}
} catch {}
// Fetch fresh
const date = `2026-${String(month).padStart(2, "0")}-15`;
const response = await fetch(
`https://worlddataapi.com/v1/travel/${destination}?date=${date}`,
{ headers: { "X-API-Key": API_KEY } },
);
const result = await response.json();
const data = result.climate;
const entry = { data, timestamp: Date.now() };
this.cache.set(key, entry);
try {
localStorage.setItem(key, JSON.stringify(entry));
} catch {}
return data;
}
async getYear(destination) {
const months = await Promise.all(
Array.from({ length: 12 }, (_, i) => this.get(destination, i + 1)),
);
return months.map((data, i) => ({
month: i + 1,
...data,
}));
}
}
const climateCache = new ClimateCache();
Styling#
.climate-overview {
max-width: 900px;
margin: 0 auto;
}
.climate-months {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
margin: 24px 0;
}
.month-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
text-align: center;
}
.month-card h3 {
margin: 0 0 8px;
font-size: 14px;
color: #666;
}
.month-card .temps {
font-size: 18px;
margin-bottom: 8px;
}
.month-card .high {
color: #ff6b6b;
font-weight: bold;
}
.month-card .low {
color: #4dabf7;
}
.month-card .details {
display: flex;
justify-content: center;
gap: 12px;
font-size: 12px;
color: #666;
}
.temperature-chart {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 24px;
}
.temperature-chart .legend {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 10px;
font-size: 12px;
}
.temperature-chart .legend .high {
color: #ff6b6b;
}
.temperature-chart .legend .low {
color: #4dabf7;
}
.best-time {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-top: 24px;
}
.best-time .recommendations {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.best-time .best h4 {
color: #28a745;
}
.best-time .avoid h4 {
color: #dc3545;
}
.best-time ul {
list-style: none;
padding: 0;
}
.best-time li {
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.best-time li strong {
display: block;
}
.best-time .details,
.best-time .reason {
font-size: 13px;
color: #666;
}
.packing-suggestions {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
margin-top: 24px;
}
.packing-suggestions .climate-summary {
color: #666;
margin-bottom: 16px;
}
.packing-suggestions .suggestion-categories {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.packing-suggestions .category h4 {
margin: 0 0 8px;
padding-bottom: 8px;
border-bottom: 2px solid #007bff;
}
.packing-suggestions .category ul {
list-style: none;
padding: 0;
}
.packing-suggestions .category li {
padding: 4px 0;
padding-left: 20px;
position: relative;
}
.packing-suggestions .category li::before {
content: "[ ]";
position: absolute;
left: 0;
}
.destination-comparison table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
.destination-comparison th,
.destination-comparison td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.destination-comparison th {
background: #f8f9fa;
font-weight: 600;
}
.destination-comparison .high {
color: #ff6b6b;
}
.destination-comparison .low {
color: #4dabf7;
}
.highlights {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 16px;
}
.highlight {
background: #e7f3ff;
padding: 12px 16px;
border-radius: 20px;
font-size: 14px;
}
.highlight .icon {
margin-right: 8px;
}
Common Pitfalls#
Treating climate data as weather forecasts. Climate data shows historical averages, not predictions. A month with 50mm average rainfall could have 0mm one year and 100mm the next. Always communicate to users that these are typical conditions, not guarantees.
Ignoring regional variation within countries. Country-level climate data represents national averages. Japan in April varies dramatically between Hokkaido (cold, possible snow) and Okinawa (warm, beach weather). For large countries, consider using city-level data when available.
Forgetting about microclimates. Coastal cities, mountain areas, and urban heat islands can differ significantly from regional averages. San Francisco's fog makes it cooler than inland California despite similar latitudes.
Over-engineering the "best time" algorithm. User preferences vary widely. Some travelers love hot weather; others avoid it. Provide the data and let users make informed decisions rather than prescribing a single "best" answer.
Making too many API calls. Climate data is stable—monthly averages change slowly over decades. Cache aggressively (30+ days) and consider pre-fetching year-round data for popular destinations to improve user experience and reduce costs.
Not handling missing data gracefully. Some regions may have incomplete climate data. Always check for null values and provide fallback UI rather than crashing or showing NaN values.
Summary#
Building a climate-aware trip planning feature helps travelers make informed decisions about when to visit and what to pack. The key points:
Use the travel endpoint with a date parameter to get monthly climate averages
Climate data represents historical averages, not real-time weather or forecasts
Cache climate data aggressively—it changes slowly and caching reduces API calls
Present data visually with temperature charts and month-by-month comparisons
Generate packing suggestions based on temperature, rainfall, and humidity
Let users compare multiple destinations for the same travel period
Always communicate that actual weather may differ from historical averages
Ready to add climate data to your travel app? Get your API key and start with the free tier, or explore the travel endpoint documentation for full details on available fields.
Next Steps#
Building a Power Adapter Checker for Travel Apps — Essential travel prep
Adding Emergency Numbers to Your Travel App — Safety features
Caching Strategies for Reference Data APIs — Optimize API usage