Testing · Localhost · Docker · Desktop app
How to test a localhost API (and why web clients can't)
Why web-based API clients cannot call localhost on their own, how to test a local API with a local bridge, a desktop client, a tunnel or a staging deploy, and the Docker networking mistakes that look like client bugs.
To test an API running on localhost, the request has to start on the same machine (or network) as the API, because localhost always means "the computer sending this request". A web-based client that sends through its own server cannot reach your laptop's localhost on its own. With Swastik Codes you can keep using the web app and run swc bridge on your computer so requests leave from there, use the desktop app for Windows and Linux, expose the port through a tunnel with a public HTTPS hostname, or deploy to a staging server. If the API runs in Docker, also check that it listens on 0.0.0.0 and that the port is published.
The rest of this guide explains why the web app cannot do it, walks through each option with concrete commands, and covers the Docker and binding problems that make a working API look unreachable.
Why an online API client cannot reach localhost
localhost is not an address on the internet. It is a name that resolves to the loopback interface, 127.0.0.1 for IPv4 and ::1 for IPv6, of whichever machine does the resolving. The same is true, in a wider sense, of private addresses such as 192.168.1.20 or 10.0.0.5: they only mean something inside your network.
A browser page is limited in what it can send. It cannot set some headers, it is subject to CORS, and it cannot open raw TCP connections for protocols such as gRPC or MQTT. So the Swastik Codes web app does not send requests from your browser tab. It sends them to the swastik.codes server, and the server makes the real request and returns the response. That is what makes custom headers, every auth type and eight protocols work in a browser.
When that server sees http://localhost:3000/health, localhost refers to the server itself, not to your laptop. For safety the server refuses private, loopback, link-local and other reserved addresses outright. A request to localhost or to 192.168.1.20 fails with an error like this:
refusing to connect to localhost: it resolves to a private or loopback addressThe check runs on the resolved IP address, not on the hostname, so a public DNS name that points at 127.0.0.1 is refused too. In the web app, the error panel then offers the local bridge and the desktop app.
Would calling localhost straight from the browser work instead?
Sometimes, but it is fragile. A page served from a public HTTPS origin that calls http://localhost:3000 needs your API to send CORS headers that allow that origin, and browsers increasingly restrict public sites from reaching local and private addresses (Chrome's Private Network Access work and the newer local network access permission). Many headers cannot be set from page JavaScript at all. A client that runs outside the browser avoids all of this.
Option 1: keep the web app and run the local bridge
The swc command-line tool has a bridge command. It connects out from your computer to swastik.codes with your API key, and while it runs, the requests and collection runs you send from the web app are executed on your computer instead of our server:
swc login # paste an API key from Account settings → API keys
swc bridgeswc bridge: requests sent from https://swastik.codes/app as you@example.com run on this computer ("dev-laptop").
✓ connected; waiting for requests
GET http://localhost:3000/health → 200 (4 ms)
POST http://192.168.1.20:8080/orders → 201 (18 ms)Then click Local bridge in the app's status bar and switch it on. There is no browser extension and no CORS to configure, and nothing on your machine is opened to the internet: the bridge only takes work from your own signed-in sessions. It covers HTTP requests (REST, GraphQL queries) and collection runs; for WebSocket, gRPC, MQTT and load tests, use the desktop app. Install swc as a single file for Linux or Windows.
Option 2: use the desktop app
The Swastik Codes desktop app runs the same interface, but requests leave from your own computer through a native Rust HTTP engine instead of the swastik.codes server. Loopback and private addresses are allowed, so all of these work:
GET http://localhost:3000/health
POST http://192.168.1.20:8080/orders
WS ws://127.0.0.1:9001/live
gRPC localhost:50051
MQTT mqtt://10.0.0.5:1883What you get:
- Every protocol locally: HTTP requests, collection runs, load tests, and WebSocket, Socket.IO, Server-Sent Events, gRPC, MQTT and MCP connections all start on your machine. The target sees your IP address, and your VPN, hosts file and network rules apply.
- No CORS: CORS is a rule browsers enforce on pages. A native client is not a page, so the local server does not need CORS headers for your tests to work.
- No account required: you can work in a local workspace without signing in. Signing in syncs your cloud workspaces, environments and teams between the desktop app and the browser.
- Platforms: Windows 10 or 11 (64-bit) and Linux x86_64 (
.deb,.rpmor AppImage). A macOS version is not available yet; on a Mac, use one of the other options below.
The desktop app is free, like the web app.
A quick check against a local server
Start any server on port 3000. A minimal one with Node.js:
// server.mjs
import { createServer } from 'node:http';
createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, path: req.url }));
}).listen(3000, '127.0.0.1', () => console.log('http://127.0.0.1:3000'));Run node server.mjs, open the desktop app, send GET http://localhost:3000/health, and you should see {"ok":true,"path":"/health"}. Add a post-response test so the check is repeatable:
swc.test('local API is up', function () {
swc.response.to.have.status(200);
swc.expect(swc.response.json().ok).to.eql(true);
});Postman's pm.* syntax runs unchanged, so existing scripts work too; see writing API tests with swc.* scripts.
Keep one collection for local, staging and production
Put the base URL in a variable rather than in every request: {{baseUrl}}/health. Create an environment per target, for example local with baseUrl = http://localhost:3000 and staging with baseUrl = https://staging.example.com, and switch between them. The environments and variables page explains scopes. In the web app, choose the staging environment; in the desktop app, either works.
HTTPS on localhost
If your local server uses a self-signed certificate or one from a local CA (for example created with mkcert), the request fails certificate verification by default. Either turn off Enable SSL certificate verification in the request's Settings tab for that request, or add your local CA certificate under Settings → Certificates so verification keeps working. The second option is better if you test HTTPS behaviour itself.
Option 3: expose localhost through a tunnel
A tunnel gives your local port a temporary public HTTPS hostname. Traffic to that hostname goes to the tunnel provider and then down a connection your machine opened, so nothing on your router needs to change. This is useful when the client must be the web app, when a teammate on another network needs to call your API, or when a third-party service has to send you a webhook.
Two widely used tools:
# ngrok (needs a free account and an auth token)
ngrok http 3000
# Cloudflare Tunnel, quick mode (prints a *.trycloudflare.com URL)
cloudflared tunnel --url http://localhost:3000Each prints a public https:// URL. Put that URL in your baseUrl variable and send from the web app as usual. Because the hostname resolves to a public address, the private-address check does not apply.
Things to know before you rely on a tunnel:
- It is public. Anyone who learns the URL can call your API. Keep authentication on, avoid real data, and stop the tunnel when you are done.
- Host header checks. Some development servers reject requests whose
Hostheader they do not recognise. Vite (server.allowedHosts) and webpack-dev-server (allowedHosts) are common examples; add the tunnel hostname there. - URLs change. Free or quick tunnels often get a new hostname each time they start. Keep it in a variable so you change it once.
- Extra latency and limits. Requests travel through the provider, so response times are not representative, and providers apply their own rate and size limits. Do not load-test through a tunnel.
- WebSocket and gRPC. Tunnels that support WebSocket work for
wss://testing. gRPC needs end-to-end HTTP/2, which not every tunnel mode provides; check your provider's documentation.
Option 4: deploy to a staging environment
For anything beyond a quick check, a staging deployment is the most realistic target: real TLS, a real domain, real proxies and load balancers in front of the service. It is also the only option that lets you run scheduled monitors and share a stable URL with the team. A small VM or a platform service is enough. Many problems that never appear on localhost, such as a proxy stripping a header, a body size limit or a missing CORS rule for your frontend, show up here first.
Docker networking gotchas
Many "cannot reach localhost" reports are not client problems at all. The API is running, but not where the client is looking.
The server listens on 127.0.0.1 inside a container
Inside a container, 127.0.0.1 is the container's own loopback interface. Docker's published ports forward traffic to the container's network interface, not to its loopback. A server that binds only to 127.0.0.1 inside the container is therefore unreachable through -p 3000:3000, and the request usually fails with a connection reset or an empty reply.
Bind to all interfaces inside the container:
# Node / Express
app.listen(3000, '0.0.0.0')
# Python
uvicorn main:app --host 0.0.0.0 --port 8000
flask run --host 0.0.0.0
# Vite dev server
vite --host 0.0.0.00.0.0.0 is a bind address meaning "every interface". It is not a destination; send your requests to localhost or the machine's IP, not to 0.0.0.0.
The port is not published
EXPOSE 3000 in a Dockerfile documents the port; it does not make it reachable from the host. Publish it when you run the container:
docker run -p 3000:3000 my-api # host port 3000 -> container port 3000
docker run -p 127.0.0.1:3000:3000 my-api # only reachable from this machineWith Docker Compose, use ports: ["3000:3000"]. Run docker ps and check the PORTS column: 0.0.0.0:3000->3000/tcp means it is published.
The API is in a container and calls something on the host
From inside a container, localhost is the container. To reach a service on the host machine, use host.docker.internal. Docker Desktop provides that name automatically. On Linux with Docker Engine, add it yourself:
docker run --add-host=host.docker.internal:host-gateway my-api# docker-compose.yml
services:
api:
extra_hosts:
- "host.docker.internal:host-gateway"Two containers talking to each other
Containers on the same user-defined network (every Compose project gets one) reach each other by service name: http://db:5432, http://api:3000. Neither localhost nor the host's published port is the right address between containers.
The server listens on 127.0.0.1 and you test from another device
The reverse case: a server bound to 127.0.0.1 on your laptop is unreachable from your phone, another computer or a VM. Bind to 0.0.0.0 (or the LAN interface) and allow the port through the firewall, then call http://<laptop-ip>:3000.
Quick diagnosis
| Symptom | Likely cause | Fix |
|---|---|---|
| "refusing to connect … private or loopback address" | Sending from the web app | Desktop app, tunnel or staging |
| Connection refused | Nothing listening on that port, or port not published | Check the process, docker ps, -p |
| Connection reset or empty reply from a container | Server bound to 127.0.0.1 inside the container | Bind to 0.0.0.0 |
| Works from the host, not from a container | localhost means the container | host.docker.internal or service name |
Certificate error on https://localhost | Self-signed or local CA certificate | Add the CA in Settings → Certificates, or turn off verification for that request |
| Tunnel URL returns 403 or "host not allowed" | Dev server checks the Host header | Add the tunnel host to allowedHosts |
Which option should you use?
- You like working in the browser: the local bridge. One command, and HTTP requests and collection runs from the web app reach your machine.
- Daily development on Windows or Linux, every protocol: the desktop app. No setup, and your collection works unchanged against staging later.
- You need the web app, a teammate, or an incoming webhook: a tunnel, for as long as the test lasts.
- Pre-release checks, monitors and shared testing: a staging deployment.
Whichever you choose, keep the address in an environment variable so the same requests, scripts and collection runs work everywhere. When your local API is ready to be tested properly, the guide on how to test a REST API online covers the next steps.
Frequently asked questions
Can I test a localhost API online without installing anything?
Almost. An online client that sends from its own server cannot see your machine's localhost by itself. With Swastik Codes, the only thing to install is the single-file swc tool: run swc bridge and switch on Local bridge in the web app. Without anything installed, the route is a tunnel that gives your local port a public HTTPS URL.
Why does Swastik Codes say "refusing to connect" for localhost?
The web app sends requests through the swastik.codes server, and that server refuses private, loopback and link-local addresses so it cannot be used to reach internal networks. Switch on the local bridge (swc bridge) or use the desktop app to send from your own computer instead.
Do I need to enable CORS on my local API to test it?
Not for the local bridge or a desktop client. CORS is enforced by browsers on web pages, so the bridge, the desktop app, curl and other native clients ignore it. You do need CORS if your own browser frontend, served from a different origin, calls the API.
What is the difference between 0.0.0.0 and 127.0.0.1?
As a bind address, 127.0.0.1 accepts connections only from the same machine (or the same container), while 0.0.0.0 accepts them on every network interface. Inside Docker, bind to 0.0.0.0 so published ports reach your server.
Is there a Swastik Codes desktop app for Mac?
Not yet. The desktop app is available for Windows and Linux. On a Mac, use the web app with a tunnel or a staging server for now.
Can I test a WebSocket or gRPC server on localhost?
Yes, with the desktop app: WebSocket, Socket.IO, SSE, gRPC, MQTT and MCP connections all start on your machine. See how to test a WebSocket API and the gRPC client guide for the protocol details.