Tracking user locations based on IP addresses is a frequent requirement for security auditing, fraud detection, and creating localized customer experiences inside Salesforce. While legacy systems relied heavily on Visualforce, modern Salesforce architecture dictates using Lightning Web Components (LWC) backed by an Apex REST callout to fetch and display this geographic data securely.
Key Points Summary
- Use standard
lightning-inputandlightning-buttonelements to capture the IP address in your LWC. - Make API callouts through Apex rather than native JavaScript
fetch()to bypass strict browser CORS restrictions. - Use
@AuraEnabledon your Apex wrapper class properties so the LWC can easily read the parsed JSON data. - Always prefer Named Credentials over old Remote Site Settings to authorize your external API endpoints securely.
Step 1: Create the Apex Callout Controller
Because browser security policies (CORS) usually block direct API requests from the frontend, our Apex controller will act as a proxy. It constructs the HttpRequest, communicates with a geolocation endpoint (like ipapi.co), and deserializes the JSON response into an LWC-friendly wrapper class.
Below is the Apex class. Notice that we throw an
AuraHandledException if the API fails, which ensures the LWC catches a clean error message.
public with sharing class IPLocationTracker {
@AuraEnabled
public static IPDetails getIPDetails(String ipAddress) {
Http http = new Http();
HttpRequest request = new HttpRequest();
// Dynamically build endpoint depending on user input
// Note: For production, use a Named Credential instead of hardcoding the URL
String endpoint = 'https://ipapi.co/';
if (String.isNotBlank(ipAddress)) {
endpoint += EncodingUtil.urlEncode(ipAddress, 'UTF-8') + '/json/';
} else {
endpoint += 'json/'; // Fetches data for the caller's IP
}
request.setEndpoint(endpoint);
request.setMethod('GET');
request.setTimeout(120000);
try {
HttpResponse response = http.send(request);
if (response.getStatusCode() == 200) {
// Deserialize the JSON directly into our Wrapper Class
return (IPDetails) JSON.deserialize(response.getBody(), IPDetails.class);
} else {
throw new CalloutException('Failed to fetch data. Status Code: ' + response.getStatusCode());
}
} catch (Exception ex) {
// Throwing AuraHandledException passes the error cleanly to the LWC
throw new AuraHandledException(ex.getMessage());
}
}
// Wrapper class matching the API's JSON response schema
// @AuraEnabled is required on each property for LWC visibility
public class IPDetails {
@AuraEnabled public String ip { get; set; }
@AuraEnabled public String country_code { get; set; }
@AuraEnabled public String country_name { get; set; }
@AuraEnabled public String region { get; set; }
@AuraEnabled public String city { get; set; }
@AuraEnabled public String postal { get; set; }
@AuraEnabled public String timezone { get; set; }
@AuraEnabled public Decimal latitude { get; set; }
@AuraEnabled public Decimal longitude { get; set; }
}
}
Step 2: Build the LWC User Interface (HTML)
Next, we build a clean, modern user interface using standard Lightning base components. We will use a lightning-card to display the results dynamically once the data is returned from Apex.
<template>
<lightning-card title="IP Geolocation Tracker" icon-name="standard:location">
<div class="slds-p-around_medium">
<!-- Input Area -->
<lightning-layout vertical-align="end" class="slds-m-bottom_medium">
<lightning-layout-item padding="around-small" size="8">
<lightning-input type="text" label="Enter IP Address" value={ipInput} onchange={handleInputChange} placeholder="e.g. 8.8.8.8"></lightning-input>
</lightning-layout-item>
<lightning-layout-item padding="around-small" size="4">
<lightning-button label="Get Details" variant="brand" onclick={fetchDetails} disabled={isLoading}></lightning-button>
</lightning-layout-item>
</lightning-layout>
<!-- Loading Spinner -->
<template if:true={isLoading}>
<lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
</template>
<!-- Error Message -->
<template if:true={errorMsg}>
<div class="slds-text-color_error slds-m-bottom_medium">
{errorMsg}
</div>
</template>
<!-- Results Area -->
<template if:true={ipData}>
<div class="slds-box slds-theme_default">
<p><strong>IP:</strong> {ipData.ip}</p>
<p><strong>City:</strong> {ipData.city}</p>
<p><strong>Region:</strong> {ipData.region}</p>
<p><strong>Country:</strong> {ipData.country_name} ({ipData.country_code})</p>
<p><strong>Zip/Postal:</strong> {ipData.postal}</p>
<p><strong>Time Zone:</strong> {ipData.timezone}</p>
<p><strong>Coordinates:</strong> {ipData.latitude}, {ipData.longitude}</p>
</div>
</template>
</div>
</lightning-card>
</template>
Step 3: Wire it all together with JavaScript
The JavaScript controller imports our Apex method, handles the input changes, and imperatively calls the backend when the user clicks the button.
import { LightningElement } from 'lwc';
import getIPDetails from '@salesforce/apex/IPLocationTracker.getIPDetails';
export default class IpTracker extends LightningElement {
ipInput = '';
ipData;
errorMsg;
isLoading = false;
// Track user input
handleInputChange(event) {
this.ipInput = event.target.value;
}
// Call Apex imperatively
fetchDetails() {
this.isLoading = true;
this.ipData = null;
this.errorMsg = null;
getIPDetails({ ipAddress: this.ipInput })
.then(result => {
this.ipData = result;
this.isLoading = false;
})
.catch(error => {
this.errorMsg = 'Error fetching details: ' + (error.body ? error.body.message : error.message);
this.isLoading = false;
});
}
}
- Missing Named Credentials: While older tutorials recommend setting up "Remote Site Settings," the modern best practice is to configure a Named Credential in Setup. This securely manages authentication and base URLs, removing hardcoded endpoints from your Apex class.
- LWC Wrapper Visibility: In Apex, if you create a custom wrapper class (like
IPDetails) but forget to add@AuraEnabledto its individual properties, the LWC will receive an empty object{}. - CORS Issues with JS Fetch: You might be tempted to use the native JavaScript
fetch()API directly inside your LWC to avoid Apex entirely. However, Salesforce Lightning blocks unauthorized external calls via Content Security Policy (CSP) and CORS rules. Calling out via Apex avoids these strict browser blocks.
Frequently Asked Questions (FAQ)
A: You can, but it is not recommended for this use case. Because the user determines exactly when the callout should fire (by clicking a button after typing an IP), an imperative Apex call (using .then() and .catch()) gives you much better control over loading spinners and error handling than the automatic @wire service.
A: Standard exceptions (like CalloutException) are often masked by the Salesforce framework for security purposes when passed to an LWC, showing a generic "Server Error". Always catch your standard exceptions in Apex and throw a new AuraHandledException with your custom message to pass the error cleanly to the frontend.
- Frontend Framework: Lightning Web Components (LWC)
- Callout Execution: Imperative Apex (triggered by a button click)
- Parsing Method:
JSON.deserialize()mapped to an@AuraEnabledWrapper Class - Security Standard: Use Named Credentials instead of hardcoded URLs and Remote Site Settings.