Skip to content

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.

License

Notifications You must be signed in to change notification settings

shahnoorgit/rn-dev-url

Repository files navigation

rn-dev-url

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.

Features

  • 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

Installation

pnpm add rn-dev-url
# or
npm install rn-dev-url
# or
yarn add rn-dev-url

Quick Start

import { 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'

API Reference

getDevApiUrl(port, options?)

Returns the appropriate API base URL for the current environment.

Parameters:

  • port (number) - The port number of your local backend
  • options (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';
}>

Behavior

Production Mode

Condition: __DEV__ === false or NODE_ENV === 'production'

Behavior:

  • ✅ Smart detection is DISABLED
  • ✅ LAN IP evaluation is DISABLED
  • prodUrl is REQUIRED (throws if missing)
  • ✅ Returns prodUrl immediately

Example:

// Production build
const config = await getDevApiUrl(3000, {
  prodUrl: 'https://api.example.com',
});
// Returns: { url: 'https://api.example.com', source: 'prod' }

Dev Mode - Default (Local Backend)

Condition: __DEV__ === true and forceProdUrlInDev !== true

Behavior:

  • Tries candidates in this order:
    1. Android emulator → http://10.0.2.2:<port>
    2. iOS simulator → http://127.0.0.1:<port>
    3. LAN device → http://<lan-ip>:<port>
  • If preferLan === true, LAN is tried before simulator defaults
  • If healthPath provided, tests connectivity for each candidate
  • Returns first reachable URL
  • Falls back to prodUrl if 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' }

Dev Mode - Remote API

Condition: __DEV__ === true and forceProdUrlInDev === true

Behavior:

  • ✅ Skips smart detection entirely
  • ✅ Immediately returns prodUrl
  • prodUrl is 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' }

Behavior Table

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

Safety Guardrails

The library guarantees:

  1. Never exposes LAN IPs in production - Smart detection is completely disabled
  2. Never runs smart detection in production - Hard-coded check prevents execution
  3. Fails predictably - Clear error messages when required options are missing
  4. No hidden magic - All behavior is explicit and documented

Troubleshooting

LAN IP Not Detected

LAN detection is best-effort only. If automatic detection fails:

  1. Set explicit override:

    export RN_DEV_HOST_IP=192.168.1.100
  2. Or use preferLan option:

    await getDevApiUrl(3000, {
      preferLan: true,
      // Will try LAN first if detected
    });

Android Emulator Not Reaching Backend

If 10.0.2.2 doesn't work, try using adb reverse:

adb reverse tcp:3000 tcp:3000

Then use localhost or 127.0.0.1 instead of the emulator IP.

Connectivity Checks Timing Out

If health checks are too slow:

  1. Increase timeout:

    await getDevApiUrl(3000, {
      healthPath: '/health',
      timeoutMs: 5000, // 5 seconds
    });
  2. Or skip health checks:

    await getDevApiUrl(3000, {
      // No healthPath - uses first candidate
    });

Using Remote Dev API

If your team uses a shared remote dev API:

await getDevApiUrl(3000, {
  prodUrl: 'https://dev-api.example.com',
  forceProdUrlInDev: true, // Skip local detection
});

Common Usage Patterns

❌ Why Top-Level Await Doesn't Work

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

✅ Solution 1: Initialize at App Startup (Recommended)

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 });

✅ Solution 2: Use Inside Async Functions

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();

✅ Solution 3: Lazy Initialization with Promise

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 });

Examples

Basic Usage

import { getDevApiUrl } from 'rn-dev-url';

const { url } = await getDevApiUrl(3000, {
  prodUrl: 'https://api.example.com',
});

const api = axios.create({ baseURL: url });

With Health Check

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}`);

Prefer LAN IP

const { url } = await getDevApiUrl(3000, {
  prodUrl: 'https://api.example.com',
  preferLan: true, // Try LAN before emulator/simulator
  healthPath: '/health',
});

Remote Dev API

const { url } = await getDevApiUrl(3000, {
  prodUrl: 'https://dev-api.example.com',
  forceProdUrlInDev: true, // Always use prodUrl in dev
});

How It Works

  1. Platform Detection: Detects Android emulator, iOS simulator, or physical device
  2. Candidate Generation: Creates URL candidates based on platform
  3. Connectivity Testing (optional): Pings health endpoint to verify reachability
  4. Selection: Returns first reachable candidate, or falls back to prodUrl

Non-Goals

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.

License

MIT

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published