Skip to content
APIs

How to Connect a Real API to an AI-Generated App

Move an API call from an insecure, client-side prototype into a proper server-side route handler with error handling.

Intermediate11 min read

The mistake most AI-generated prototypes make

Ask an AI tool for "a weather widget that calls a weather API" and it will often generate a `fetch` call directly inside a client component with the API key inlined in the code. That works in a demo and leaks your key to anyone who opens devtools in production. This tutorial fixes that pattern properly.

Step 1: Store the key server-side only

Any environment variable prefixed with `NEXT_PUBLIC_` is bundled into client-side JavaScript and is publicly visible. Secrets must never use that prefix — keep them as plain variable names, which are only available on the server.

.env.local
WEATHER_API_KEY=your_key_here

Step 2: Create a server-side route handler

In the Next.js App Router, a `route.ts` file inside `app/api/` runs only on the server. The client never sees the API key — it only ever talks to your own route.

app/api/weather/route.ts
import { NextResponse } from "next/server"

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const city = searchParams.get("city")

  if (!city) {
    return NextResponse.json({ error: "Missing 'city' parameter" }, { status: 400 })
  }

  const url = `https://api.example-weather.com/v1/current?key=${process.env.WEATHER_API_KEY}&q=${encodeURIComponent(city)}`

  const res = await fetch(url, { next: { revalidate: 300 } })

  if (!res.ok) {
    return NextResponse.json({ error: "Upstream weather API error" }, { status: 502 })
  }

  const data = await res.json()
  return NextResponse.json(data)
}

Step 3: Call your own route from the client

components/weather-widget.tsx
"use client"

import { useState } from "react"

export function WeatherWidget() {
  const [city, setCity] = useState("")
  const [result, setResult] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)

  async function handleSearch() {
    setLoading(true)
    try {
      const res = await fetch(`/api/weather?city=${encodeURIComponent(city)}`)
      const data = await res.json()
      setResult(res.ok ? data.current.summary : data.error)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div>
      <input value={city} onChange={(e) => setCity(e.target.value)} placeholder="City" />
      <button onClick={handleSearch} disabled={loading}>
        {loading ? "Searching…" : "Search"}
      </button>
      {result ? <p>{result}</p> : null}
    </div>
  )
}

Step 4: Handle errors and loading states honestly

  • Show a real loading state — don't let the button look clickable while a request is in flight
  • Surface upstream errors to the user in plain language, not a raw stack trace
  • Set a request timeout for third-party APIs that might hang instead of failing fast
  • Log server-side errors somewhere you'll actually see them, not just `console.log`

Step 5: Think about rate limits and caching

Most third-party APIs rate-limit you, and calling them on every single page load is wasteful. The `next: { revalidate: 300 }` option above caches the response for 5 minutes at the fetch level — adjust it to match how fresh the data actually needs to be.

Prompting an AI tool to build this correctly the first time

Prompt
Add a weather lookup feature. Create a server-side route handler at
app/api/weather/route.ts that reads WEATHER_API_KEY from a non-public
environment variable, calls the weather API, and returns JSON. Do not put
the API key in any client component. Add a client component that calls
this route and handles loading and error states explicitly.

Ready to build the next one?

Browse the full tutorial library or grab a ready-made prompt for your next step.