1 min read

Named Localhost URLs Without the Config Soup

Why ports are a terrible way to remember your local services — and how named URLs fix it in a single line.

Nilan SahaNilan Saha

If you run more than two services locally, you already know the pain: was the API on 3000 or 3001? Was that the docs site or the dashboard? Ports are an implementation detail leaking straight into your muscle memory.

Named URLs replace that guessing game with something you can actually remember.

The problem with ports

Ports are allocated, not chosen. Your tooling grabs whatever is free, your teammates get different numbers, and nothing is stable across restarts. A README that says "open http://localhost:5173" is wrong the moment Vite decides 5173 is taken.

Map a name to a port

With Portline you point a human-readable name at a port and forget the number ever existed:

# Give the running Vite dev server a stable local name
portline map dashboard --port 5173

# Now this always works, regardless of which port Vite grabbed
open http://dashboard.localhost

The mapping survives restarts and is shared through a small config file you can commit:

# portline.toml
[routes]
dashboard = 5173
api       = 8080
docs      = 4321

Resolve names in code

Because the names are deterministic, you can reference them directly instead of threading port numbers through environment variables:

// lib/services.ts
const services = {
  api: "http://api.localhost",
  docs: "http://docs.localhost",
} as const;

export const fetchUser = async (id: string) => {
  const res = await fetch(`${services.api}/users/${id}`);
  if (!res.ok) throw new Error(`user ${id} failed: ${res.status}`);
  return res.json();
};

No more process.env.API_PORT ?? 8080 sprinkled across the codebase.

Why it matters

A name is a contract. A port is an accident.

Stable names make onboarding faster, READMEs accurate, and shared configs portable. The number was never the point — the service was.