Skip to main content

How to Make Dynamic HTTP Callouts in Salesforce Using LWC and Apex

In plain words: A dynamic HTTP callout in LWC allows users to input target API endpoints, custom headers, and request bodies directly from a Lightning UI, sending those parameters to an Apex controller that securely executes the REST callout and returns the real-time response.

Modern enterprise applications frequently need to test, query, or sync with third-party web services. By combining Lightning Web Components with Apex HTTP classes, you can build a flexible API client inside Salesforce to send GET, POST, or PUT requests to any configured external endpoint on demand.

Prerequisites

  • A Salesforce Developer Edition, Sandbox, or Scratch Org.
  • Salesforce CLI (sf) installed and authenticated.
  • Basic knowledge of REST APIs, JSON formatting, and Apex HTTP classes.
  • Target external URLs registered in Remote Site Settings or configured via Named Credentials.

Step 1: Set Up Remote Site Settings or Named Credentials

Warning Trap: Salesforce blocks all outbound HTTP/HTTPS calls to external URLs that are not explicitly authorized. Before running any callout, navigate to Setup > Security > Remote Site Settings and register your target endpoint domain (or use a Named Credential for authenticated endpoints).

Step 2: Create the Apex Controller

Create an Apex class named HttpCalloutController.cls. This controller receives the dynamic request parameters, constructs the HttpRequest, and handles status codes and error responses safely:

public with sharing class HttpCalloutController {
    
    @AuraEnabled
    public static String makeHttpRequest(String endpoint, String method, String headersJson, String payload) {
        try {
            if (String.isBlank(endpoint)) {
                throw new AuraHandledException('Endpoint URL cannot be empty.');
            }

            Http http = new Http();
            HttpRequest request = new HttpRequest();
            request.setEndpoint(endpoint.trim());
            request.setMethod(String.isNotBlank(method) ? method.toUpperCase() : 'GET');
            request.setTimeout(120000); // Max timeout: 120 seconds

            // Parse and attach dynamic JSON headers if provided
            if (String.isNotBlank(headersJson)) {
                Map<String, Object> headersMap = (Map<String, Object>) JSON.deserializeUntyped(headersJson);
                for (String key : headersMap.keySet()) {
                    request.setHeader(key, String.valueOf(headersMap.get(key)));
                }
            }

            // Set default Content-Type header if not provided
            if (request.getHeader('Content-Type') == null) {
                request.setHeader('Content-Type', 'application/json');
            }

            // Attach request body for POST, PUT, or PATCH
            if (String.isNotBlank(payload) && request.getMethod() != 'GET') {
                request.setBody(payload);
            }

            HttpResponse response = http.send(request);
            
            // Package response details
            Map<String, Object> result = new Map<String, Object>{
                'statusCode' => response.getStatusCode(),
                'status' => response.getStatus(),
                'body' => response.getBody()
            };

            return JSON.serializePretty(result);
        } catch (Exception ex) {
            throw new AuraHandledException('Callout failed: ' + ex.getMessage());
        }
    }
}

Step 3: Build the LWC Template

Create an LWC named httpCalloutLWC. Open httpCalloutLWC.html and build the input form with SLDS styling, a method dropdown, and a formatted response preview area:

<template>
    <lightning-card title="Dynamic HTTP Callout Tester" icon-name="utility:connected_apps">
        <div class="slds-p-around_medium">
            
            <template lwc:if={isLoading}>
                <lightning-spinner alternative-text="Sending request..." size="small"></lightning-spinner>
            </template>

            <div class="slds-grid slds-gutters slds-m-bottom_small">
                <div class="slds-col slds-size_1-of-4">
                    <lightning-combobox
                        name="method"
                        label="HTTP Method"
                        value={method}
                        options={methodOptions}
                        onchange={handleInputChange}>
                    </lightning-combobox>
                </div>
                <div class="slds-col slds-size_3-of-4">
                    <lightning-input
                        name="endpoint"
                        label="Endpoint URL"
                        placeholder="https://jsonplaceholder.typicode.com/posts"
                        value={endpoint}
                        onchange={handleInputChange}>
                    </lightning-input>
                </div>
            </div>

            <div class="slds-m-bottom_small">
                <lightning-textarea
                    name="headers"
                    label="Headers (JSON format)"
                    placeholder='{"Authorization": "Bearer token", "Accept": "application/json"}'
                    value={headers}
                    onchange={handleInputChange}>
                </lightning-textarea>
            </div>

            <div class="slds-m-bottom_medium">
                <lightning-textarea
                    name="payload"
                    label="Request Body (Payload)"
                    placeholder='{"title": "Test Title", "body": "Sample content", "userId": 1}'
                    value={payload}
                    onchange={handleInputChange}>
                </lightning-textarea>
            </div>

            <lightning-button
                variant="brand"
                label="Send HTTP Request"
                onclick={handleMakeRequest}
                disabled={isLoading}>
            </lightning-button>

            <template lwc:if={response}>
                <div class="slds-m-top_large">
                    <h3 class="slds-text-heading_small slds-m-bottom_x-small">Response Output:</h3>
                    <pre class="ap-pre"><code>{response}</code></pre>
                </div>
            </template>

        </div>
    </lightning-card>
</template>

Step 4: Implement the JavaScript Controller Logic

In httpCalloutLWC.js, manage user input state, call the Apex method imperatively, and catch client/server exceptions:

import { LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import makeHttpRequest from '@salesforce/apex/HttpCalloutController.makeHttpRequest';

export default class HttpCalloutLWC extends LightningElement {
    endpoint = 'https://jsonplaceholder.typicode.com/posts/1';
    method = 'GET';
    headers = '';
    payload = '';
    response = '';
    isLoading = false;

    get methodOptions() {
        return [
            { label: 'GET', value: 'GET' },
            { label: 'POST', value: 'POST' },
            { label: 'PUT', value: 'PUT' },
            { label: 'DELETE', value: 'DELETE' }
        ];
    }

    handleInputChange(event) {
        const { name, value } = event.target;
        this[name] = value;
    }

    async handleMakeRequest() {
        if (!this.endpoint) {
            this.showToast('Validation Error', 'Please enter a valid Endpoint URL.', 'warning');
            return;
        }

        this.isLoading = true;
        this.response = '';

        try {
            const rawResponse = await makeHttpRequest({
                endpoint: this.endpoint,
                method: this.method,
                headersJson: this.headers,
                payload: this.payload
            });
            this.response = rawResponse;
            this.showToast('Success', 'HTTP Callout executed successfully.', 'success');
        } catch (error) {
            this.response = error?.body?.message || JSON.stringify(error);
            this.showToast('Callout Error', error?.body?.message || 'Failed to complete callout.', 'error');
        } finally {
            this.isLoading = false;
        }
    }

    showToast(title, message, variant) {
        this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
    }
}
360 Architecture Summary:
  • Imperative Apex: HTTP callouts cannot use @wire because callouts have side effects and mutate external state.
  • Heap Limits: Ensure response payloads do not exceed the synchronous Apex 6 MB transaction heap size limit.
  • CORS & Security: Browser-based fetch() in LWC is subject to Lightning Locker/LWS CORS rules; routing callouts through Apex ensures reliable server-to-server connectivity.

Step 5: Expose and Deploy to Lightning Pages

Deployment Steps:
  • Ensure <isExposed>true</isExposed> is set in httpCalloutLWC.js-meta.xml with targets for lightning__AppPage and lightning__RecordPage.
  • Deploy component and controller: sf project deploy start.
  • Open Lightning App Builder in Setup and place the component on your testing page.
  • Test using a public test REST endpoint like https://jsonplaceholder.typicode.com/posts/1.
Core Takeaway: Routing dynamic HTTP callouts through an Apex controller provides server-level control, bypasses browser CORS limitations, and lets you handle authentication tokens and headers securely.