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.
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.
WEATHER_API_KEY=your_key_hereStep 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.
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
"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
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.Tools used in this tutorial
Claude Code
A terminal-based agentic coding tool from Anthropic that reads, edits and runs code in your own repository.
Cursor
An AI-native code editor built on VS Code, with deep inline editing, chat, and agent capabilities.
OpenAI Codex
OpenAI's cloud and CLI coding agent that can work on tasks in an isolated sandboxed environment.
Prompts to pair with this
Integrate a third-party API with proper error handling and retries
Gets an AI to build a production-grade API client — authentication, retries, rate limiting, and typed responses — instead of a bare fetch call.
Copy promptdatabaseDesign a normalized SQL schema with indexes and constraints
Turns a plain-language description of your data into a normalized SQL schema with sensible indexes, foreign keys, and constraints.
Copy promptRelated tutorials
How to Deploy a Next.js App Built with AI
Take a Next.js project from your local machine to a live, publicly accessible URL with environment variables handled correctly.
IntermediateHow to Debug AI-Generated Code
A repeatable method for tracking down bugs in AI-generated code — reproduce, isolate, prompt with real context, and verify with a test.
IntermediateHow to Build a SaaS MVP with AI
A realistic, end-to-end path for building a SaaS MVP with AI tools — from schema and auth to billing, testing, and first users.
AdvancedReady to build the next one?
Browse the full tutorial library or grab a ready-made prompt for your next step.