---
title: How to Build a Weather API with Nitro and Vercel
description: Provide real-time weather data to apps and websites with a single Nitro route, Vercel cache storage, and Observability.
url: /kb/guide/weather-api-with-nitro
canonical_url: "https://vercel.com/kb/guide/weather-api-with-nitro"
published: 2025-11-03
last_updated: 2026-07-16
authors: Pooya Pasa, Ismael Rumzan
related:
  - /docs/observability
  - /docs/functions
  - /docs/fluid-compute
  - /docs/data-cache
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

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:

1. Accepts a city name and geocodes it to coordinates
   
2. Fetches current weather data from the Open-Meteo API
   
3. Returns temperature, humidity, wind speed, and other weather metrics with optional metric/imperial unit conversion
   

## Prerequisites

- Node.js and pnpm installed locally
  
- A Vercel account
  
- Basic understanding of Nitro and caching concepts
  

## Build the Weather API

### 1\. Create your Nitro project

Start with the template [Deploy Nitro to Vercel](https://vercel.com/templates/backend/nitro-starter). Once your the project is deployed on Vercel, clone the repository locally.

### 2\. Add the Weather route

Create `server/routes/api/weather/[city].ts` and paste the following code:

```typescript
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;
});
```

### 3\. Test the route locally

Install the dependencies and launch the application in dev mode:

```bash
pnpm install
pnpm dev
```

Use `curl` to test the API route:

```bash
# Test with default metric units
curl http://localhost:3000/api/weather/london
 
# Test with imperial units
curl "http://localhost:3000/api/weather/san%20francisco?units=imperial"
```

### 4\. Configure Observability

Observability helps you monitor your API's performance, error rates, and cache hit ratios in production.

To use [Observability](https://vercel.com/docs/observability), update the `nitro.config.ts`:

```typescript
import { defineNitroConfig } from 'nitro/config';
 
export default defineNitroConfig({
  srcDir: 'server',
  // Enables routing hints for Vercel Function insights
  compatibilityDate: '2025-07-15', // or 'latest'
});
```

### 5\. Deploy to Vercel

- 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
  

> When deployed to Vercel, Nitro server routes become [Vercel Functions](https://vercel.com/docs/functions) and use [Fluid compute](https://vercel.com/docs/fluid-compute) by default

## Using observability

Now that you deployed your Nitro app to Vercel, let's check that Observability is working.

1. Make a `curl` request to your production URL: `https://my-nitro-app.vercel.app/api/weather/london.`
   
2. Go to the Vercel functions section of the Observability tab for your project by selecting your team and project [here](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fobservability%2Fvercel-functions&title=Go+to+Observability+for+Vercel+Functions).
   
3. 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.     ## Cache your responses You can cache your API responses by using [Vercel runtime cache](https://vercel.com/docs/data-cache) with Nitro.

First, update the `nitro.config.ts` to use the Vercel cache driver:

```typescript
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:

```typescript
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](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fobservability%2Fruntime-cache&title=Go+to+Observability+for+Runtime+Cache).
  
- 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.
  

## Summary

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
  

## Next steps

### Hands-on challenges

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
  

### Explore references

- [Explore the Nitro docs](https://nitro.build/guide)
  
- [Nitro Cache](https://nitro.build/guide/cache)
  
- [Vercel Functions](https://vercel.com/docs/functions)
  
- [Vercel Observability](https://vercel.com/docs/observability)