To provide real-time weather data, you need to handle API integration, caching for performance, and scaling for traffic spikes. With Vercel functions and Nitro, you can build a production-ready weather API with Vercel cache and observability in minutes.
In this tutorial, you will build and deploy a weather API route using the Open-Meteo API that:
- Accepts a city name and geocodes it to coordinates
- Fetches current weather data from the Open-Meteo API
- Returns temperature, humidity, wind speed, and other weather metrics with optional metric/imperial unit conversion
- Node.js and pnpm installed locally
- A Vercel account
- Basic understanding of Nitro and caching concepts
Start with the template Deploy Nitro to Vercel. Once your the project is deployed on Vercel, clone the repository locally.
Create server/routes/api/weather/[city].ts
and paste the following code:
import { getRouterParam, getQuery, createError } from 'h3';import { $fetch } from 'ofetch'; export default defineEventHandler(async (event) => { const cityParam = getRouterParam(event, 'city'); if (!cityParam) { throw createError({ statusCode: 400, statusMessage: 'city is required' }); } const city = decodeURIComponent(cityParam); const { units } = getQuery(event) as { units?: 'metric' | 'imperial' }; const normalizedUnits = units === 'imperial' ? 'imperial' : 'metric'; // 1) Geocode city -> lat/lon const geo = await $fetch<{ results?: Array<{ name: string; country: string; latitude: number; longitude: number; }>; }>('https://geocoding-api.open-meteo.com/v1/search', { params: { name: city, count: 1, language: 'en', format: 'json' }, }); if (!geo?.results?.length) { throw createError({ statusCode: 404, statusMessage: 'city not found' }); } const { name, country, latitude, longitude } = geo.results[0]; // 2) Fetch current weather const forecastParams: Record<string, any> = { latitude, longitude, current: [ 'temperature_2m', 'relative_humidity_2m', 'apparent_temperature', 'wind_speed_10m', ].join(','), timezone: 'auto', }; if (units === 'imperial') { forecastParams.temperature_unit = 'fahrenheit'; forecastParams.wind_speed_unit = 'mph'; } const forecast = await $fetch<{ current: { time: string; temperature_2m: number; relative_humidity_2m: number; apparent_temperature: number; wind_speed_10m: number; }; }>('https://api.open-meteo.com/v1/forecast', { params: forecastParams }); const weatherData = { city: name, country, latitude, longitude, units: normalizedUnits, current: forecast.current, }; return weatherData;});
Install the dependencies and launch the application in dev mode:
pnpm installpnpm dev
Use curl
to test the API route:
# Test with default metric unitscurl http://localhost:3000/api/weather/london # Test with imperial unitscurl "http://localhost:3000/api/weather/san%20francisco?units=imperial"
Observability helps you monitor your API's performance, error rates, and cache hit ratios in production.
To use Observability, update the nitro.config.ts
:
import { defineNitroConfig } from 'nitro/config'; export default defineNitroConfig({ srcDir: 'server', // Enables routing hints for Vercel Function insights compatibilityDate: '2025-07-15', // or 'latest'});
- Push the changes to your remote repository
- Vercel will create a new preview deployment for your to test
- Merge to main branch to deploy to Production
Now that you deployed your Nitro app to Vercel, let's check that Observability is working.
- Make a
curl
request to your production URL:https://my-nitro-app.vercel.app/api/weather/london.
- Go to the Vercel functions section of the Observability tab for your project by selecting your team and project here.
- Under the list of routes, you should see
/api/weather/[city]
with detailed metrics such as number of invocations, active CPU time and error rate.
You can cache your API responses by using Vercel runtime cache with Nitro.
First, update the nitro.config.ts
to use the Vercel cache driver:
import { defineNitroConfig } from 'nitro/config'; export default defineNitroConfig({ srcDir: 'server', // Enables routing hints for Vercel Function insights compatibilityDate: '2025-07-15', // or 'latest' storage: { '/cache/nitro': { // It's important to use this specific path for saving to Vercel runtime cache. Other paths will use the browser cache. driver: 'vercel-runtime-cache', }, },});
Then, update the routes/api/weather/[city].ts
with the cache options by using the defineCachedEventHandler
helper. In this example, we are caching the response for 1 hr:
import { getRouterParam, getQuery, createError } from 'h3';import { $fetch } from 'ofetch'; export default defineCachedEventHandler( async (event) => { const cityParam = getRouterParam(event, 'city'); if (!cityParam) { throw createError({ statusCode: 400, statusMessage: 'city is required' }); } const city = decodeURIComponent(cityParam); const { units } = getQuery(event) as { units?: 'metric' | 'imperial' }; const normalizedUnits = units === 'imperial' ? 'imperial' : 'metric'; // 1) Geocode city -> lat/lon const geo = await $fetch<{ results?: Array<{ name: string; country: string; latitude: number; longitude: number; }>; }>('https://geocoding-api.open-meteo.com/v1/search', { params: { name: city, count: 1, language: 'en', format: 'json' }, }); if (!geo?.results?.length) { throw createError({ statusCode: 404, statusMessage: 'city not found' }); } const { name, country, latitude, longitude } = geo.results[0]; // 2) Fetch current weather const forecastParams: Record<string, any> = { latitude, longitude, current: [ 'temperature_2m', 'relative_humidity_2m', 'apparent_temperature', 'wind_speed_10m', ].join(','), timezone: 'auto', }; if (units === 'imperial') { forecastParams.temperature_unit = 'fahrenheit'; forecastParams.wind_speed_unit = 'mph'; } const forecast = await $fetch<{ current: { time: string; temperature_2m: number; relative_humidity_2m: number; apparent_temperature: number; wind_speed_10m: number; }; }>('https://api.open-meteo.com/v1/forecast', { params: forecastParams }); const weatherData = { city: name, country, latitude, longitude, units: normalizedUnits, current: forecast.current, }; return weatherData; }, { maxAge: 3600, // Cache for 1 hour },);
Check the runtime cache metric in the Observability tab
- Push the changes to your remote repository and merge to the
main
branch to deploy to Production. - Make a few
curl
requests to your production URL,https://my-nitro-app.vercel.app/api/weather/london
, with a few seconds interval between each request. - Go to the Vercel Runtime cache section of the Observability tab for your project by selecting your team and project here.
- Make sure the time interval is set on Last hour. You should see a Cache Read for each request. The Hit Rate will summarize the number of Hits and Miss to the cache.
In this tutorial, you’ve built a cached, observable, real-time weather API using Nitro on Vercel.
You learned to:
- Structure a dynamic API route and integrate external APIs
- Use Vercel runtime cache to cache your responses
- Monitor the route performance and cache usage using Vercel Observability
Extend your knowledge by:
- Adding precipitation and cloud cover to the response
- Implementing rate limiting to protect the endpoint
- Building a front-end widget to display the weather data