Hardcoded Localhost and Dev URLs: The Quiet Bug That Breaks Everything
By Seaworthy · 19 September 2026
I recently reviewed a pull request where an AI assistant had generated a fetching function with http://localhost:3000 baked into the source. The developer accepted it, ran the app locally, saw it work, and moved on. Two days later, the staging deploy failed because the frontend tried to call a backend that did not exist on the user's machine. This is not a rare story. It happens constantly, and it is boring until it costs you an afternoon.
The pattern is simple: you need a URL for an API, a database, or a feature flag service. You type the one that works on your laptop. Maybe you leave a comment like // TODO: change for prod. That comment becomes permanent. The code ships. Then someone else runs it in a container, or a CI job, or a staging environment, and the app silently talks to the wrong place. Or it crashes with a connection refused error that makes no sense to anyone who did not write the line.
Why does this keep happening? Because local development is the first environment you see, and AI coding assistants are very good at giving you something that runs immediately. They optimize for the next five minutes. They do not know your deployment topology. They do not know that localhost means something different inside a Docker network, a Kubernetes pod, or a serverless function. So they hardcode the obvious thing. And you, tired and focused on the feature, accept it.
I get it. Environment variables feel like ceremony. Creating a config module feels like overkill for a small project. But the cost of not doing it is not just one bug. It is a class of bugs that show up at the worst time: during a demo, during an incident, or when a new teammate tries to run the app for the first time. The fix is not complicated. It is just slightly less convenient upfront.
Here is the bad pattern:
// bad: hardcoded dev URL
async function fetchUser(id) {
const response = await fetch(`http://localhost:4000/users/${id}`);
return response.json();
}
That code works on the author's machine. It fails everywhere else. A slightly better version reads from an environment variable with a fallback for local development:
// better: configurable with a local fallback
const API_BASE = process.env.API_BASE_URL || 'http://localhost:4000';
async function fetchUser(id) {
const response = await fetch(`${API_BASE}/users/${id}`);
return response.json();
}
Is that perfect? No. The fallback still points to localhost, so if you forget to set API_BASE_URL in production, you get the same failure. Some teams prefer to throw an error if the variable is missing outside development. That is a stronger choice. But even the fallback version makes the dependency visible. It tells the next reader that this value changes by environment.
The real problem is not the string localhost. It is the assumption that the code knows where it runs. Good code asks the environment for that information. Bad code decides for itself. This is a basic ops idea: configuration belongs outside the artifact. Your build should be the same whether it runs on your laptop or in a cluster. The only thing that changes is the environment it reads.
I have seen teams try to solve this with build-time replacements, like swapping strings during a bundling step. That can work, but it hides the dependency. A build that rewrites localhost to api.example.com is still a build that knows about production. A cleaner approach is to pass the URL at runtime through environment variables, command-line flags, or a config service. The application reads it. The deployment provides it.
There is also a security angle. Hardcoded URLs often come with hardcoded credentials or tokens. I have seen http://admin:password@localhost:5984 in source code. That is a secret in version control. Even if the URL is harmless, the habit is dangerous. Once you accept one hardcoded value, you accept the next one. The fix for URLs is the same fix for secrets: move them out of source and into the environment where they belong.
What should you do today? Search your codebase for localhost, 127.0.0.1, and common dev ports like 3000, 4000, 5000, and 8080. Do not just search application code. Check tests, scripts, Dockerfiles, and CI configs. Some of those are fine. A test that spins up a local server should use localhost. But a production fetch call should not. If you use AI assistants, add a note to your prompts: "Do not hardcode URLs. Read from environment variables." It helps, though you still need to review the output. The assistant will not enforce your deployment rules for you.
This is not a glamorous topic. It is ops basics. But ops basics are what keep your app running when the demo is live and the room is full. Fixing this takes ten minutes. Ignoring it takes a weekend. I know which one I prefer.
If you want a simple way to catch these patterns early, Seaworthy scans source code for hardcoded localhost and dev URLs so they do not reach production.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.