How to Test Your Mobile App Backend on a Real Device Without Deploying
Your API runs on localhost. Your Android or iOS app runs on a physical device. The device cannot reach localhost, it has no idea your laptop even exists. This guide explains exactly why that is, what the options are for different frameworks, and why a Localtonet HTTP tunnel is the cleanest solution when the same-network tricks stop working.
📋 What's in this guide
Why localhost Does Not Work on a Real Device
When your backend runs on http://localhost:3000, the address localhost
refers to the machine that process is running on your laptop.
When your mobile app makes a request to http://localhost:3000,
the device resolves localhost as itself the phone.
There is no server running on the phone's port 3000, so the connection fails.
Emulators handle this differently from real devices.
The Android emulator maps 10.0.2.2 to the host machine's localhost,
so you can use that IP inside the emulator and reach your laptop's server.
The iOS simulator shares the same network stack as your Mac, so localhost
actually works in the simulator. Neither of these tricks works on a physical device.
| Environment | To reach host machine localhost | Notes |
|---|---|---|
| Android Emulator | 10.0.2.2:PORT |
Special alias built into the emulator |
| iOS Simulator | localhost:PORT |
Shares the Mac's network stack |
| Android Real Device | Local IP, ADB reverse, or tunnel | No automatic alias, needs explicit setup |
| iOS Real Device | Local IP or tunnel | Must be on the same Wi-Fi, or use a tunnel |
10.0.2.2:PORTlocalhost:PORTThe Three Approaches
There are three practical ways to connect a real device to a local backend. Each has trade-offs that make it more or less suitable depending on your situation.
1. Use your machine's local network IP
Find your laptop's IP on the local Wi-Fi network (e.g. 192.168.1.42)
and replace localhost with that IP in your app's API base URL.
Simple to set up but requires both devices to be on the same Wi-Fi network.
Breaks the moment you or your tester moves to a different network, uses mobile data,
or the IP changes after a router restart.
2. ADB reverse (Android only)
Android Debug Bridge can create a reverse port forwarding tunnel over the USB cable. Your device connects to your laptop as if its own localhost is the laptop's localhost. Works reliably but requires a USB cable, Android developer tools installed, and USB debugging enabled on the device. Does not work for iOS or wireless testing.
3. Localtonet HTTP tunnel
Your backend gets a public HTTPS URL. The app points at that URL instead of localhost. Works on any device, any network, Android and iOS, with or without a cable. The tester can be on the other side of the world. The URL stays the same as long as the tunnel is running. This is the only option that works for remote testers and CI/CD pipelines.
When to use each approach
- Local IP: quick solo testing on the same Wi-Fi, no extra tools
- ADB reverse: Android development with a USB cable already connected
- Localtonet tunnel: remote testers, different networks, iOS, CI/CD, push notification testing
When each approach breaks
- Local IP: different network, mobile data, IP changes
- ADB reverse: iOS devices, wireless testing, remote testers
- Localtonet tunnel: nothing, works everywhere with internet access
Approach 1: Local Network IP
Find your laptop's local network IP:
# macOS and Linux
ipconfig getifaddr en0 # Wi-Fi interface
# or
ip route get 1.1.1.1 | awk '{print $7; exit}'
# Windows (PowerShell)
Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.IPAddress -like "192.168.*"}
Use this IP as the base URL in your app wherever you currently have localhost.
For example, if your API runs on port 3000 and your laptop's IP is 192.168.1.42:
// Instead of this
const BASE_URL = 'http://localhost:3000';
// Use this
const BASE_URL = 'http://192.168.1.42:3000';
Android 9 and above blocks unencrypted HTTP traffic by default.
A local IP URL starting with http:// will be blocked.
Add a network security config to your AndroidManifest.xml
to allow cleartext traffic for your local IP during development,
or use the tunnel approach which provides HTTPS automatically.
Approach 2: ADB Reverse (Android)
ADB reverse makes your Android device treat its own localhost as your laptop's localhost.
Connect the device via USB with USB debugging enabled, then run:
# Forward port 3000 from the device to your laptop
adb reverse tcp:3000 tcp:3000
# For multiple ports
adb reverse tcp:3000 tcp:3000
adb reverse tcp:8080 tcp:8080
# Verify active reverse tunnels
adb reverse --list
After running this, your app can use http://localhost:3000 as the base URL
and the request reaches your laptop's server. No IP address changes needed.
The tunnel stays active until you disconnect the USB cable or run adb reverse --remove-all.
Apple does not provide a public equivalent of ADB for iOS devices. For real iOS device testing over a local backend, use the local IP approach or a Localtonet tunnel.
Approach 3: Localtonet HTTP Tunnel
An HTTP tunnel gives your local backend a public HTTPS URL. Your app uses that URL as the API base, and it works from any device on any network.
Start your backend server
Make sure your API is running locally. Note the port it is listening on.
Install Localtonet and create an HTTP tunnel
Authenticate Localtonet with your token, then go to the
HTTP tunnel page,
set local IP to 127.0.0.1 and the port your backend is running on.
Click Create and start the tunnel.
The dashboard shows a public HTTPS URL such as https://abc123.localto.net.
Use the tunnel URL as your API base URL in the app
Replace localhost with your tunnel URL in the app's API configuration.
No USB cable, no same-network requirement, no cleartext restrictions.
const BASE_URL = 'https://abc123.localto.net';
This URL works from an Android device on mobile data, an iPhone on a different Wi-Fi network, a tester on the other side of the world, and a CI pipeline running in the cloud. All of them hit your laptop's server in real time.
React Native and Expo
React Native projects typically store the API base URL in a constants file or environment variable.
With Expo, you can use app.config.js to set different values per environment.
Basic React Native setup
// api/config.js
const DEV_BASE_URL = 'https://abc123.localto.net';
const PROD_BASE_URL = 'https://api.yourapp.com';
export const BASE_URL = __DEV__ ? DEV_BASE_URL : PROD_BASE_URL;
Expo with app.config.js
// app.config.js
export default {
name: 'MyApp',
extra: {
apiBaseUrl: process.env.API_BASE_URL || 'https://abc123.localto.net',
},
};
// Using the value in your app
import Constants from 'expo-constants';
const BASE_URL = Constants.expoConfig.extra.apiBaseUrl;
# Start with your tunnel URL as an env variable
API_BASE_URL=https://abc123.localto.net npx expo start
Using .env files with Expo
# .env.development
EXPO_PUBLIC_API_URL=https://abc123.localto.net
# .env.production
EXPO_PUBLIC_API_URL=https://api.yourapp.com
// Access anywhere in your app
const BASE_URL = process.env.EXPO_PUBLIC_API_URL;
The Metro bundler (the dev server that serves your JS bundle) and your API backend are two different things. The tunnel in this guide exposes your API backend only. Metro still runs on your laptop and the device connects to it via the normal Expo/RN development setup (local IP or USB). You do not need to tunnel Metro unless you are testing on a completely remote device.
Flutter
Flutter apps typically define the base URL in a constants file. Using the tunnel URL is straightforward, replace localhost with the tunnel address.
Constants file approach
// lib/constants/api.dart
class ApiConfig {
static const String baseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://abc123.localto.net',
);
}
// Using it in a service
import 'package:http/http.dart' as http;
import '../constants/api.dart';
Future<dynamic> fetchUsers() async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}/api/users'),
);
return response.body;
}
Passing the URL at run time
# Run with a custom base URL via --dart-define
flutter run --dart-define=API_BASE_URL=https://abc123.localto.net
If you use the local IP approach on Android, add android:usesCleartextTraffic="true"
to your AndroidManifest.xml application tag for development builds.
With the Localtonet tunnel, this is not needed the tunnel URL is HTTPS.
Frequently Asked Questions
My Android app gets a cleartext traffic error when using the local IP. How do I fix it?
Android 9 and above blocks HTTP traffic by default. The cleanest fix for development is to use the Localtonet tunnel, which gives you HTTPS automatically. If you want to keep the local IP approach, create a network security config file at res/xml/network_security_config.xml, add your local IP as a permitted cleartext domain, and reference it from AndroidManifest.xml. Remove this config before releasing to production.
Can I share the tunnel URL with a remote QA tester?
Yes. Share the tunnel URL and the tester's device connects to your local backend from wherever they are. The only requirement is that your laptop is running the backend and the Localtonet tunnel is active. The tester does not need to install anything or be on the same network.
Does the tunnel URL change every time I restart Localtonet?
The subdomain on HTTP tunnels can be made consistent by reserving it. In the Localtonet dashboard, you can set a custom subdomain for your HTTP tunnel so the URL stays the same across restarts. This is useful when you have the URL hardcoded in a test build or shared with a QA team. Alternatively, attach a custom domain for a completely permanent URL.
Can I use this for push notification testing that requires a public webhook URL?
Yes. Push notification providers like APNs (Apple) and FCM (Google) sometimes need to call a webhook on your server to deliver status updates or receipts. A Localtonet HTTP tunnel gives them a reachable public HTTPS URL pointing at your local server. This is one of the scenarios where the local IP and ADB reverse approaches simply do not work the provider's servers cannot reach your local network.
Will the app try to use the tunnel URL in production builds?
Only if you let it. The code examples in this guide use environment variables or the __DEV__ flag to switch between the tunnel URL in development and the real API URL in production. Make sure your build pipeline sets the correct API_BASE_URL variable for production builds. The tunnel URL pointing at your laptop should never end up in a production release.
I work with a backend developer on a different machine. Can I tunnel their server?
Yes, if they run Localtonet on their machine and share the tunnel URL with you. Each developer creates their own tunnel for their own local server. You point your app at their tunnel URL to test against their backend, and they point theirs at yours. This way both of you can test integration against each other's local code without deploying anything.
Give Your Local Backend a Public URL in Under a Minute
Create a free Localtonet account, open an HTTP tunnel for your backend port, and paste the HTTPS URL into your app. Test on any real device, any network, instantly.
Create Free Localtonet Account →