Automatically resolves the correct API base URL for React Native / Expo apps in development. Makes dev API resolution work automatically when using a local backend, while never exposing LAN IPs or performing smart detection in production.
- ✅ Zero native dependencies - Pure TypeScript/JavaScript
- ✅ Works everywhere - Expo Go, Expo Dev Client, and Bare React Native
- ✅ Production-safe - Never runs smart detection or exposes LAN IPs in production
- ✅ Predictable behavior - Clear rules, no hidden magic
- ✅ Small bundle size - Minimal footprint
pnpm add rn-dev-url
# or
npm install rn-dev-url
# or
yarn add rn-dev-urlimport { getDevApiUrl } from 'rn-dev-url';
// In your API configuration
const apiConfig = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
healthPath: '/health', // optional
});
console.log(apiConfig.url); // http://10.0.2.2:3000 (Android emulator)
console.log(apiConfig.source); // 'android-emulator'Returns the appropriate API base URL for the current environment.
Parameters:
port(number) - The port number of your local backendoptions(object, optional):prodUrl(string, optional) - Required in production, optional in dev. The production/remote API URL.healthPath(string, optional) - Optional path to test connectivity (e.g., '/health'). If provided, will ping each candidate.preferLan(boolean, optional) - If true, try LAN IP before simulator defaults. Default:false.forceProdUrlInDev(boolean, optional) - If true, skip smart detection in dev and immediately use prodUrl. Default:false.log(boolean, optional) - Enable developer logs. Default:false.timeoutMs(number, optional) - Connectivity timeout in milliseconds. Default:2500.
Returns:
Promise<{
url: string;
source: 'prod' | 'android-emulator' | 'ios-simulator' | 'lan-device' | 'fallback';
}>Condition: __DEV__ === false or NODE_ENV === 'production'
Behavior:
- ✅ Smart detection is DISABLED
- ✅ LAN IP evaluation is DISABLED
- ✅
prodUrlis REQUIRED (throws if missing) - ✅ Returns
prodUrlimmediately
Example:
// Production build
const config = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
});
// Returns: { url: 'https://api.example.com', source: 'prod' }Condition: __DEV__ === true and forceProdUrlInDev !== true
Behavior:
- Tries candidates in this order:
- Android emulator →
http://10.0.2.2:<port> - iOS simulator →
http://127.0.0.1:<port> - LAN device →
http://<lan-ip>:<port>
- Android emulator →
- If
preferLan === true, LAN is tried before simulator defaults - If
healthPathprovided, tests connectivity for each candidate - Returns first reachable URL
- Falls back to
prodUrlif no local backend reachable - Returns empty URL if no candidate and no
prodUrl
Example:
// Development with local backend
const config = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com', // fallback
healthPath: '/health',
});
// Returns: { url: 'http://10.0.2.2:3000', source: 'android-emulator' }Condition: __DEV__ === true and forceProdUrlInDev === true
Behavior:
- ✅ Skips smart detection entirely
- ✅ Immediately returns
prodUrl - ✅
prodUrlis REQUIRED (throws if missing)
Use cases:
- Frontend-only developers
- Remote dev / staging APIs
- Shared backend environments
- CI preview builds
Example:
// Development with remote dev API
const config = await getDevApiUrl(3000, {
prodUrl: 'https://dev-api.example.com',
forceProdUrlInDev: true,
});
// Returns: { url: 'https://dev-api.example.com', source: 'prod' }| Mode | prodUrl |
forceProdUrlInDev |
Behavior |
|---|---|---|---|
| Production | ✅ Required | N/A | Returns prodUrl, no detection |
| Production | ❌ Missing | N/A | Throws error |
| Dev (default) | Optional | false |
Tries local candidates, falls back to prodUrl |
| Dev (default) | ❌ Missing | false |
Tries local candidates, returns empty URL if none reachable |
| Dev (remote) | ✅ Required | true |
Returns prodUrl, skips detection |
| Dev (remote) | ❌ Missing | true |
Throws error |
The library guarantees:
- ✅ Never exposes LAN IPs in production - Smart detection is completely disabled
- ✅ Never runs smart detection in production - Hard-coded check prevents execution
- ✅ Fails predictably - Clear error messages when required options are missing
- ✅ No hidden magic - All behavior is explicit and documented
LAN detection is best-effort only. If automatic detection fails:
-
Set explicit override:
export RN_DEV_HOST_IP=192.168.1.100 -
Or use
preferLanoption:await getDevApiUrl(3000, { preferLan: true, // Will try LAN first if detected });
If 10.0.2.2 doesn't work, try using adb reverse:
adb reverse tcp:3000 tcp:3000Then use localhost or 127.0.0.1 instead of the emulator IP.
If health checks are too slow:
-
Increase timeout:
await getDevApiUrl(3000, { healthPath: '/health', timeoutMs: 5000, // 5 seconds });
-
Or skip health checks:
await getDevApiUrl(3000, { // No healthPath - uses first candidate });
If your team uses a shared remote dev API:
await getDevApiUrl(3000, {
prodUrl: 'https://dev-api.example.com',
forceProdUrlInDev: true, // Skip local detection
});You cannot use await at the top level of a module like this:
// ❌ This will fail with "await is not defined"
import { getDevApiUrl } from 'rn-dev-url';
export const apiBaseUrl = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
});Why? Top-level await requires:
- ES2022+ target in TypeScript
- Module type set to
"ESNext"or"ES2022" - Your bundler/build tool to support it
- React Native/Expo may not support it in all environments
Create an apibase.ts file in your project:
// apibase.ts
import { getDevApiUrl } from 'rn-dev-url';
let cachedUrl: string | null = null;
export async function getApiBaseUrl(): Promise<string> {
if (cachedUrl) {
return cachedUrl;
}
const result = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
healthPath: '/health', // optional
});
cachedUrl = result.url;
return result.url;
}Then initialize it at app startup:
// App.tsx or index.js
import { getApiBaseUrl } from './apibase';
// Initialize at startup
const apiBaseUrl = await getApiBaseUrl();
// Use it throughout your app
const api = axios.create({ baseURL: apiBaseUrl });Call getDevApiUrl inside async functions where you need it:
// apibase.ts
import { getDevApiUrl } from 'rn-dev-url';
export async function initializeApi() {
const { url } = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
});
return axios.create({ baseURL: url });
}
// Usage
const api = await initializeApi();Export a promise that resolves to the URL:
// apibase.ts
import { getDevApiUrl } from 'rn-dev-url';
export const apiBaseUrlPromise = getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
}).then(result => result.url);
// Usage in other files
import { apiBaseUrlPromise } from './apibase';
const url = await apiBaseUrlPromise;
const api = axios.create({ baseURL: url });import { getDevApiUrl } from 'rn-dev-url';
const { url } = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
});
const api = axios.create({ baseURL: url });const { url, source } = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
healthPath: '/health',
log: true, // See which URL was selected
});
console.log(`Using ${source}: ${url}`);const { url } = await getDevApiUrl(3000, {
prodUrl: 'https://api.example.com',
preferLan: true, // Try LAN before emulator/simulator
healthPath: '/health',
});const { url } = await getDevApiUrl(3000, {
prodUrl: 'https://dev-api.example.com',
forceProdUrlInDev: true, // Always use prodUrl in dev
});- Platform Detection: Detects Android emulator, iOS simulator, or physical device
- Candidate Generation: Creates URL candidates based on platform
- Connectivity Testing (optional): Pings health endpoint to verify reachability
- Selection: Returns first reachable candidate, or falls back to
prodUrl
This library does NOT implement:
- ❌ Proxying
- ❌ Port forwarding
- ❌ Tunnels
- ❌ Automatic
adb reverse - ❌ IPv6 support
- ❌ Network scanning
The library prioritizes predictability and safety over clever features.
MIT