Switching image providers in Next.js with Vercel's AI Gateway
I built a Next.js app with App Router to switch models through the AI Gateway by changing one line, and where the official docs still leave gaps.

I built a Next.js app with App Router to switch models through the AI Gateway by changing one line, and where the official docs still leave gaps.
What attracted me to Vercel's AI Gateway was the promise of "one key, hundreds of models": a single endpoint, with automatic fallback between providers and, according to the official docs, no markup on tokens (including in Bring Your Own Key mode). I decided to test this in practice in a Next.js app, isolating the model name so I could switch providers by changing the minimum amount of code.
Editor's note: the title of this tutorial mentions switching image providers, but the code demonstrated and tested here covers only text generation (generateText), which is what the official AI Gateway documentation presents with verifiable examples. The image generation part was neither implemented nor run here: the docs consulted don't expose an image endpoint or image model name, so the author chose not to simulate working code that might not exist. Treat the image section as conceptual guidance, not as something ready for production without prior validation against your catalog.
An honest and important note: the AI Gateway documentation I used includes code examples only for chat/text with
generateText(the famous question about the capital of France). Embeddings only appear as a listed feature, with no example. And there is no image generation example, nor an image endpoint exposed, on the Gateway's main page. That's why this tutorial focuses on what's verifiable today (text via AI SDK) and handles the image part honestly: I show the path, but I make clear what you need to confirm in your catalog before shipping.
Project setup
I started with a clean App Router. I use explicit flags to avoid the interactive prompts that the current CLI triggers (turbopack, eslint, tailwind, src dir, import alias):
npx create-next-app@latest gateway-ai --ts --app --eslint --no-tailwind --no-src-dir --import-alias "@/*" --yes
cd gateway-ai
npm i aiThe ai package is Vercel's AI SDK, and the Gateway docs confirm it works with AI SDK v5 and v6. It's what I'll actually use in the Route Handler, so no orphaned installs.
Gateway authentication is token-based. I created .env.local:
AI_GATEWAY_API_KEY=seu_token_aquiFirst stumble: I put the token in a Client Component thinking I'd "test it quickly." Never do this: the key leaks into the bundle sent to the browser. Any call that uses the key has to run on the server (Route Handler or Server Action). The AI SDK reads AI_GATEWAY_API_KEY from the environment automatically, so it's enough for the variable to exist on the server.
Server-side Route Handler
I created app/api/gen/route.ts. The core idea is to isolate the model name in a constant, so switching providers means changing one line. Here I use the AI SDK's generateText, exactly as the docs show, passing the model in the provider/model format:
import { generateText } from 'ai';
// Switching providers = changing this string. Check the names in Browse models.
const MODEL = 'anthropic/claude-opus-5'; // e.g.: 'xai/grok-4.5', 'openai/gpt-5.6-sol'
export async function POST(req: Request) {
const { prompt } = await req.json();
const start = performance.now();
const { text } = await generateText({
model: MODEL,
prompt,
});
const latencyMs = Math.round(performance.now() - start);
return Response.json({ text, latencyMs, model: MODEL });
}Notice that the only thing coupled to the provider is the MODEL string. Switching providers in the catalog (or configuring fallback via provider options) doesn't require touching anything else. This is the real advantage of the Gateway, which the docs sum up as "switch between providers and models with minimal code changes": my handler's contract doesn't change. The model names (anthropic/claude-opus-5, xai/grok-4.5, openai/gpt-5.6-sol) are the ones that appear in the docs' own examples, so check which ones are available in your account via Browse models.
What about image generation?
That was the original goal of this piece, and here I need to be transparent: the AI Gateway page I consulted doesn't document an image endpoint in the main example. The AI SDK exposes experimental_generateImage, and there's an "Image Generation" page in the docs' link map, but I'm not going to paste a made-up API path here as if it were official, because that would just make you hit a 404 and blame your own code.
The verifiable path is the same pattern as the handler above, just swapping the SDK function for experimental_generateImage after confirming, in your catalog, the exact name of the exposed image model. The structure of "isolating the model in a constant and changing one line" stays identical. If you depend on this in production, validate the name and the response format in the dashboard first, because catalogs and names change fast.
Consuming it on the client with feedback
On the component side, the focus is perceived performance: showing a loading state and not blocking the UI while the model responds. The classic bug here is making the fetch and forgetting to set the state with the result, so the screen never updates. Pay attention to setText:
'use client';
import { useState } from 'react';
export default function Gen() {
const [text, setText] = useState('');
const [busy, setBusy] = useState(false);
const [ms, setMs] = useState(0);
async function run() {
setBusy(true);
try {
const r = await fetch('/api/gen', {
method: 'POST',
body: JSON.stringify({ prompt: 'Explique HTTP/2 em uma frase' }),
});
const d = await r.json();
setText(d.text); // <- without this, nothing shows up on screen
setMs(d.latencyMs);
} finally {
setBusy(false);
}
}
return (
<div>
<button onClick={run} aria-busy={busy} disabled={busy}>
{busy ? 'Gerando...' : 'Gerar'}
</button>
{text && <p>{text}</p>}
{ms > 0 && <p>Latência: {ms}ms</p>}
</div>
);
}An accessibility detail that's often missing from AI demos: aria-busy on the button communicates the state to screen readers, since the wait is long, and disabled prevents duplicate triggers. If you adapt this for images, remember to give an alt that describes the actual content (the prompt already gives you that for free), not a generic "generated image".
Measuring before and after
My measurement was deliberately simple: timing the call with performance.now() on the server and returning latencyMs in the JSON. Don't trust "gut feeling" about which provider is faster; measure it in your own environment and with your own prompt.
I ran the same prompt, changing only the MODEL constant. What's worth noting:
- Latency varies a lot by model and output size. A prompt that asks for a short response visibly reduces the time.
- The Gateway's Observability dashboard gives aggregated latency and spend by provider, so I cross-checked my local measurement against their numbers.
- Cost: since the docs state zero markup on tokens (including with BYOK), comparing providers through the dashboard is direct, with no need for a separate calculation.
Is it worth it?
For those already in the Vercel ecosystem, the concrete gain is operational: one key, model switching via a string, and automatic fallback if a provider goes down. My handler's code doesn't know about any specific provider, which is great for testing alternatives without refactoring. The honest caveat is for images: the Gateway's main documentation still doesn't expose this in a ready-to-copy-and-paste way, so validate the model and endpoint at Browse models and on the Image Generation page before promising image generation to your team.
Translated from the Brazilian Portuguese original · Read the original
Jev turns design system into a decision engine for AI agents
TypeSafe AI's model doesn't generate interface: it chooses among options you define. This changes what it means to maintain a design system.

