Skip to main content

Override API Endpoint

This package allows backend developers and QA teams to override API endpoints for local testing or staging environments without recompiling the app. The endpoint is persisted using shared_preferences and can be changed dynamically via deeplinks.

Use Case

Perfect for scenarios where you need to:
  • Test against a local backend server during development
  • Switch between staging and production environments
  • Point to different API servers without rebuilding the app
  • Enable QA teams to test against specific backend instances

Installation

Add override_api_endpoint to your pubspec.yaml:
flutter pub add override_api_endpoint
flutter pub add shared_preferences
You’ll also need a deeplink handling package like uni_links or app_links to receive the override deeplinks.

How It Works

1

Default Endpoint

The app starts with a default API endpoint defined in code.
2

Deeplink Override

Send a specially formatted deeplink to the app with the new endpoint URL.
3

Persistence

The new endpoint is stored in SharedPreferences and persists across app restarts.
4

Auto-Load

On subsequent launches, the persisted endpoint is automatically loaded.

Basic Usage

import 'package:override_api_endpoint/override_api_endpoint.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uni_links/uni_links.dart';

final apiEndpoint = await overrideApiEndpoint(
  sharedPreferences: await SharedPreferences.getInstance(),
  getInitialUri: getInitialUri,
  deeplinkOverrideSegment: 'override',
  deeplinkQueryParameter: 'apiAddress',
  defaultEndpoint: Uri.parse('https://api.example.com'),
);

print('Using API endpoint: $apiEndpoint');

Configuration

Function Parameters

overrideApiEndpoint Parameters

Future<Uri> overrideApiEndpoint({
  required SharedPreferences sharedPreferences,
  required FutureOr<Uri?> Function() getInitialUri,
  required String deeplinkOverrideSegment,
  required String deeplinkQueryParameter,
  required Uri defaultEndpoint,
  String apiEndpointKey = 'apiAddress',
})
Parameters:
  • sharedPreferences: Instance of SharedPreferences for storing the endpoint
  • getInitialUri: Function that returns the initial deeplink URI (e.g., from uni_links)
  • deeplinkOverrideSegment: Path segment that identifies the override deeplink (e.g., 'override')
  • deeplinkQueryParameter: Query parameter name containing the new endpoint (e.g., 'apiAddress')
  • defaultEndpoint: Fallback endpoint used when no override is set
  • apiEndpointKey: SharedPreferences key for storing the endpoint (default: 'apiAddress')
The deeplink should follow this pattern:
{scheme}://{host}/{deeplinkOverrideSegment}?{deeplinkQueryParameter}={url_encoded_endpoint}
Example:
app://app/override?apiAddress=https%3A%2F%2Fapi.staging.example.com
  • Scheme: app://
  • Host: app
  • Override segment: override
  • Query parameter: apiAddress
  • Encoded endpoint: https%3A%2F%2Fapi.staging.example.com (URL-encoded)

Complete Example

import 'package:flutter/material.dart';
import 'package:override_api_endpoint/override_api_endpoint.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uni_links/uni_links.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Get the API endpoint (may be overridden via deeplink)
  final apiEndpoint = await overrideApiEndpoint(
    sharedPreferences: await SharedPreferences.getInstance(),
    getInitialUri: getInitialUri,
    deeplinkOverrideSegment: 'override',
    deeplinkQueryParameter: 'apiAddress',
    defaultEndpoint: Uri.parse('https://api.production.example.com'),
  );

  runApp(MyApp(apiEndpoint: apiEndpoint));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.apiEndpoint});

  final Uri apiEndpoint;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('API Override Example')),
        body: Center(
          child: Text('Using endpoint: $apiEndpoint'),
        ),
      ),
    );
  }
}

Testing the Override

Using ADB (Android)

# Override to local server
adb shell am start -W -a android.intent.action.VIEW \
  -d "app://app/override?apiAddress=http%3A%2F%2F10.0.2.2%3A8080" \
  com.example.app

# Override to staging
adb shell am start -W -a android.intent.action.VIEW \
  -d "app://app/override?apiAddress=https%3A%2F%2Fapi.staging.example.com" \
  com.example.app

# Clear override (return to default)
adb shell am start -W -a android.intent.action.VIEW \
  -d "app://app/override?apiAddress=clear" \
  com.example.app

Using xcrun (iOS Simulator)

# Override to local server
xcrun simctl openurl booted "app://app/override?apiAddress=http%3A%2F%2Flocalhost%3A8080"

# Clear override
xcrun simctl openurl booted "app://app/override?apiAddress=clear"

Generating URL-Encoded Endpoints

You can use online URL encoders or command-line tools:
# Using Python
python3 -c "import urllib.parse; print(urllib.parse.quote('https://api.staging.example.com'))"

# Using Node.js
node -e "console.log(encodeURIComponent('https://api.staging.example.com'))"

Clearing the Override

To reset to the default endpoint, use the special value clear:
app://app/override?apiAddress=clear
This removes the persisted endpoint from SharedPreferences and returns to using defaultEndpoint.

Advanced Usage

Custom Storage Key

If you need to store multiple endpoints or avoid naming conflicts:
final apiEndpoint = await overrideApiEndpoint(
  sharedPreferences: prefs,
  getInitialUri: getInitialUri,
  deeplinkOverrideSegment: 'override-api',
  deeplinkQueryParameter: 'endpoint',
  defaultEndpoint: Uri.parse('https://api.example.com'),
  apiEndpointKey: 'custom_api_endpoint_key',
);

Multiple Environments

// Main API
final mainApiEndpoint = await overrideApiEndpoint(
  sharedPreferences: prefs,
  getInitialUri: getInitialUri,
  deeplinkOverrideSegment: 'override-main',
  deeplinkQueryParameter: 'mainApi',
  defaultEndpoint: Uri.parse('https://api.example.com'),
  apiEndpointKey: 'main_api_endpoint',
);

// Analytics API
final analyticsEndpoint = await overrideApiEndpoint(
  sharedPreferences: prefs,
  getInitialUri: getInitialUri,
  deeplinkOverrideSegment: 'override-analytics',
  deeplinkQueryParameter: 'analyticsApi',
  defaultEndpoint: Uri.parse('https://analytics.example.com'),
  apiEndpointKey: 'analytics_api_endpoint',
);

Integration with Dependency Injection

class ApiConfig {
  static Future<ApiConfig> initialize() async {
    final endpoint = await overrideApiEndpoint(
      sharedPreferences: await SharedPreferences.getInstance(),
      getInitialUri: getInitialUri,
      deeplinkOverrideSegment: 'override',
      deeplinkQueryParameter: 'apiAddress',
      defaultEndpoint: Uri.parse('https://api.example.com'),
    );

    return ApiConfig._(endpoint);
  }

  ApiConfig._(this.endpoint);

  final Uri endpoint;

  String get baseUrl => endpoint.toString();
}

// In main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final apiConfig = await ApiConfig.initialize();

  runApp(MyApp(apiConfig: apiConfig));
}

Best Practices

Security Considerations

  • Only enable endpoint override in debug/staging builds
  • Validate endpoint URLs before using them
  • Use HTTPS endpoints in production
  • Consider adding authentication for override deeplinks

Development Workflow

  • Document available endpoints for your team
  • Create shell scripts for common overrides
  • Add override instructions to your README
  • Test override functionality during QA

Troubleshooting

  • Verify the deeplink is correctly formatted and URL-encoded
  • Check that deeplinkOverrideSegment matches the path in your deeplink
  • Ensure getInitialUri is correctly configured
  • Restart the app after sending the deeplink
  • Ensure SharedPreferences is initialized properly
  • Check that the app has permission to write to storage
  • Verify apiEndpointKey is unique and not being overwritten

Package Information

Dependencies

  • shared_preferences: ^2.0.6 - Persistent storage
This package only provides the endpoint override logic. You’ll need to add a deeplink handling package like uni_links or app_links separately.

Build docs developers (and LLMs) love