Quick Answer
AI code generators hardcode http://localhost:3000 as the API base URL because that's the address the app runs on during the conversation context used to generate the code. When you deploy to Vercel, Railway, or any production host, every API call fails because localhost doesn't exist in the production environment. The fix is to use environment variables for all URLs and never hardcode hostnames.
Why This Happens in AI-Generated Code
Bolt, Lovable, Cursor, and v0 generate code based on context - and the context they have is your local development environment. When you describe a feature that calls an API, the AI generates a working example with a concrete URL rather than a placeholder. The most concrete, reliable URL it knows about is http://localhost:3000 - the default port for Next.js, Vite, and most Node.js dev servers.
The result is code that works perfectly during local development and breaks completely on first deployment. This is one of the most common support issues for founders deploying AI-generated apps to Vercel or Netlify for the first time.
According to a 2024 Stack Overflow Developer Survey analysis, hardcoded environment-specific configuration is identified as a code smell by 78% of senior engineers but appears in the majority of first-deployment codebases from non-professional developers. For vibe-coded applications, the rate is effectively 100% without deliberate correction.
The Patterns AI Tools Generate
// ❌ BAD - Hardcoded localhost URL in fetch call
const response = await fetch('http://localhost:3000/api/users');
// ❌ BAD - Hardcoded in a constant (slightly better but still wrong)
const API_BASE = 'http://localhost:3000';
const response = await fetch(`${API_BASE}/api/users`);
// ❌ BAD - Hardcoded in Next.js API route
export async function POST(req: Request) {
const webhookUrl = 'http://localhost:3000/api/webhook/stripe';
await registerWebhook(webhookUrl);
}
// ❌ BAD - Hardcoded CORS origin in Express/Node.js backend
app.use(cors({ origin: 'http://localhost:3000' }));
// ❌ BAD - Hardcoded database or service URL
const SUPABASE_URL = 'http://localhost:54321';
Each of these patterns causes silent or loud failures in production. The fetch calls return network errors. The CORS configuration rejects all production traffic. The Supabase URL points to a local Docker container that doesn't exist on the production server.
The Correct Pattern: Environment Variables
// ✅ GOOD - Environment variable with fallback for local dev
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000';
const response = await fetch(`${API_BASE}/api/users`);
// ✅ GOOD - Server-side environment variable (not exposed to browser)
export async function POST(req: Request) {
const appUrl = process.env.APP_URL;
if (!appUrl) throw new Error('APP_URL environment variable is not set');
await registerWebhook(`${appUrl}/api/webhook/stripe`);
}
// ✅ GOOD - CORS origin from environment variable
app.use(cors({
origin: process.env.ALLOWED_ORIGIN ?? 'http://localhost:3000'
}));
// ✅ GOOD - .env.local for development, .env.production for production
// .env.local
NEXT_PUBLIC_API_URL=http://localhost:3000
APP_URL=http://localhost:3000
// Vercel dashboard environment variables (production):
NEXT_PUBLIC_API_URL=https://yourapp.vercel.app
APP_URL=https://yourapp.vercel.app
Environment Variable Strategy by Deployment Platform
| Platform | Where to Set Variables | Local Development |
|---|---|---|
| Vercel | Project Settings → Environment Variables | .env.local (gitignored) |
| Netlify | Site Settings → Environment Variables | .env.local (gitignored) |
| Railway | Project → Variables tab | .env (gitignored) |
| Fly.io | fly secrets set KEY=value |
.env (gitignored) |
| Docker / VPS | docker run -e KEY=value or compose |
.env file with docker compose |
A critical detail for Next.js: only environment variables prefixed with NEXT_PUBLIC_ are included in the browser-side JavaScript bundle. URLs for client-side fetch calls need the NEXT_PUBLIC_ prefix. URLs used only in server-side code (API routes, Server Components, Server Actions) should not use the prefix, keeping them out of the client bundle entirely.
The .env.example File Pattern
A best practice that AI tools almost never generate is the .env.example file. This is a committed, non-secret file that lists every required environment variable with placeholder values. It serves as documentation for anyone deploying the app:
// ✅ GOOD - .env.example committed to Git
NEXT_PUBLIC_API_URL=https://your-app-domain.com
APP_URL=https://your-app-domain.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
The actual .env.local file with real values must be in .gitignore. The example file uses placeholder strings so developers know what variables to configure without exposing real credentials.
How to Find and Fix Hardcoded URLs in Your Codebase
- Search your source files for localhost: Run a text search for
localhostacross all.ts,.tsx,.js, and.jsxfiles. Every occurrence in a string literal is a candidate for replacement. - Check for port-specific patterns: Look for patterns like
:3000,:8080,:4000- these are almost always hardcoded dev server addresses. - Review your .env files: Ensure
.env.local,.env.development, and.env.productionhave the correct values, and that none of them are committed to Git. - Run automated scanning: Tools like VibeDoctor (vibedoctor.io) automatically scan your codebase for hardcoded localhost URLs and environment configuration issues (CFG-002, CFG-005, CFG-006, CFG-007) and flag specific file paths and line numbers. Free to sign up.
- Create .env.example: After identifying all environment variables your app needs, create this file in your project root and commit it. Future deployments become self-documenting.
FAQ
Should I use NEXT_PUBLIC_ prefix for my API URL?
It depends on where the fetch call runs. If you're calling your own API from a client component (runs in the browser), use NEXT_PUBLIC_API_URL. If you're calling it from a Server Component, Server Action, or API route (runs on the server), use a non-prefixed variable like API_URL. Never prefix secrets like API keys or service role keys with NEXT_PUBLIC_, as this exposes them in the browser bundle.
Does Vercel automatically set any URL environment variables?
Yes. Vercel automatically sets VERCEL_URL (the deployment URL, e.g. your-app-abc123.vercel.app), VERCEL_ENV (production, preview, or development), and NEXT_PUBLIC_VERCEL_URL. You can use these to construct absolute URLs without hardcoding your domain. However, VERCEL_URL changes per deployment, so for stable production URLs, set your own APP_URL variable with your custom domain.
What about Supabase local development with Docker?
Supabase's local development stack runs on http://localhost:54321 by default. Your local .env.local should point to this address, while your production environment variables point to your Supabase project URL on supabase.co. Never commit either set - use .env.example with placeholder values to document which variables are required.
Can I use relative URLs instead of environment variables?
For same-origin API calls in a Next.js app, yes. A fetch to /api/users (relative path) works in both development and production because it's always relative to the current domain. This is the simplest approach for Next.js apps where the frontend and API are deployed together. Use absolute URLs only when calling a separate service on a different domain.
How do I handle different URLs for preview deployments on Vercel?
Vercel supports separate environment variable values for Production, Preview, and Development environments. Set your API URL to your production domain for the Production environment, and use VERCEL_URL or a staging domain for Preview. This lets preview deployments function correctly without hardcoding preview URLs in your source code.