Salesforce

Static Resources in Salesforce: Upload, Reference, and Use in Apex and LWC

```html

What Are Static Resources?

A Static Resource is a file — or a zip archive of files — stored inside your Salesforce org and served directly from Salesforce's content delivery infrastructure. JavaScript libraries, CSS files, images, fonts, JSON data files, and entire single-page application bundles all belong here. Unlike documents stored in ContentVersion or Attachment, Static Resources are version-controlled through deployments, reference-able in Visualforce and LWC, and accessible in Apex at runtime via PageReference — making them the correct tool whenever you need a file that is part of your application code rather than user data.

The key facts you must carry into every design decision:

Uploading Static Resources

Through Setup UI

  1. Navigate to Setup → Custom Code → Static Resources.
  2. Click New.
  3. Enter a unique Name (alphanumeric, underscores only — this is the API name).
  4. Choose your file. If you are uploading a folder structure, zip it first.
  5. Set Cache Control. Use Public for libraries and images that are not user-specific. Use Private for anything tied to a user session.
  6. Save.

The MIME type is inferred from the file extension. For zips, Salesforce stores the archive and lets you reference individual paths inside it using a trailing path suffix on the resource URL.

Through Metadata API / SFDX

For version-controlled projects this is the correct approach. The directory structure is:

force-app/
└── main/
    └── default/
        └── staticresources/
            ├── chartjs.resource          ← the actual file or zip
            ├── chartjs.resource-meta.xml ← metadata descriptor
            ├── myStyles.resource
            └── myStyles.resource-meta.xml

The -meta.xml descriptor for a single JS file looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
    <cacheControl>Public</cacheControl>
    <contentType>application/javascript</contentType>
    <description>Chart.js 4.4 minified bundle</description>
</StaticResource>

For a zip archive the contentType is application/zip. Deploy with:

sf project deploy start --source-dir force-app/main/default/staticresources

Referencing Static Resources in Apex

Apex gives you two ways to reach a static resource at runtime: the PageReference approach for getting a URL string, and direct content retrieval via StaticResourceCallout patterns. The most common use case is generating a URL to pass into a response or to embed in dynamic Visualforce output.

Getting the Resource URL

public class StaticResourceHelper {

    /**
     * Returns the absolute URL for a top-level static resource.
     * Suitable for single-file resources (JS, CSS, image).
     */
    public static String getResourceUrl(String resourceName) {
        PageReference ref = PageReference.forResource(resourceName);
        // getUrl() returns a relative path: /resource/TIMESTAMP/NAME
        return ref.getUrl();
    }

    /**
     * Returns the URL for a specific file inside a zip resource.
     * pathInsideZip must NOT start with a leading slash.
     * Example: getZipFileUrl('chartjsBundle', 'chart.umd.min.js')
     */
    public static String getZipFileUrl(String resourceName, String pathInsideZip) {
        PageReference ref = PageReference.forResource(resourceName, pathInsideZip);
        return ref.getUrl();
    }
}

PageReference.forResource(String name) and its two-argument variant PageReference.forResource(String name, String path) are the canonical way to resolve static resource URLs in Apex. The URL they return includes a cache-busting version timestamp that Salesforce manages automatically on each deployment — you never hard-code this timestamp.

Reading Static Resource Content in Apex

There are scenarios where you need to read the content of a static resource at runtime — for example, loading a JSON configuration file or reading an XML template. Query the StaticResource sObject, which exposes the file body as a Blob.

public class StaticResourceReader {

    /**
     * Reads a text-based static resource (JSON, XML, CSV, etc.)
     * and returns its content as a String.
     * Throws QueryException if the resource does not exist.
     */
    public static String readTextResource(String resourceName) {
        StaticResource sr = [
            SELECT Id, Body
            FROM StaticResource
            WHERE Name = :resourceName
            LIMIT 1
        ];
        return sr.Body.toString();
    }

    /**
     * Parses a JSON static resource directly into a Map.
     * The resource must contain a valid JSON object at its root.
     */
    public static Map readJsonResource(String resourceName) {
        String rawJson = readTextResource(resourceName);
        return (Map) JSON.deserializeUntyped(rawJson);
    }
}

The Body field on StaticResource returns a Blob. Calling .toString() on it gives you the UTF-8 string content. This pattern is ideal for feature-flag JSON files or lookup tables that change infrequently and do not belong in Custom Metadata (because they are too large or structured as arbitrary JSON).

Practical Example — Loading a Country Code Map

// Static resource Name: 'CountryCodeMap'
// Content: {"US":"United States","GB":"Great Britain","IN":"India", ...}

public class CountryCodeService {

    private static Map codeMap;

    private static Map getCodeMap() {
        if (codeMap == null) {
            codeMap = StaticResourceReader.readJsonResource('CountryCodeMap');
        }
        return codeMap;
    }

    public static String getCountryName(String isoCode) {
        Object name = getCodeMap().get(isoCode.toUpperCase());
        return name != null ? (String) name : 'Unknown';
    }
}

The lazy-loaded static variable means the SOQL fires once per transaction regardless of how many times getCountryName is called — an important governor limit consideration.

Referencing Static Resources in Lightning Web Components

LWC uses the @salesforce/resourceUrl scoped import to get the URL of a static resource at component load time. The platform resolves the versioned URL for you — no Apex call required.

Single-File Resource

// myImageCard.js
import { LightningElement } from 'lwc';
import myLogo from '@salesforce/resourceUrl/CloudEzeeLogo';

export default class MyImageCard extends LightningElement {
    logoUrl = myLogo;
}
<!-- myImageCard.html -->
<template>
    <img src={logoUrl} alt="CloudEzee Logo" />
</template>

The imported value is a fully-qualified URL string. Assign it directly to a property and bind it in template markup.

Zip Resource — Accessing Nested Files

When your static resource is a zip, the import gives you the base URL. Append the internal file path using string concatenation. Suppose you have a zip named chartjsBundle with this structure:

chartjsBundle.zip
├── chart.umd.min.js
├── chart.umd.min.js.map
└── LICENSE
// chartWidget.js
import { LightningElement, track } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import chartjsBase from '@salesforce/resourceUrl/chartjsBundle';

export default class ChartWidget extends LightningElement {
    @track chartInitialized = false;
    @track error;

    connectedCallback() {
        // loadScript returns a Promise; handle both resolve and reject
        loadScript(this, chartjsBase + '/chart.umd.min.js')
            .then(() => {
                this.chartInitialized = true;
                this.initializeChart();
            })
            .catch(err => {
                this.error = err.message;
                console.error('Chart.js failed to load:', err);
            });
    }

    initializeChart() {
        const canvas = this.template.querySelector('canvas');
        // Chart is now available as a global from the loaded script
        // eslint-disable-next-line no-undef
        new Chart(canvas, {
            type: 'bar',
            data: {
                labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
                datasets: [{
                    label: 'Monthly Revenue',
                    data: [12000, 19000, 14000, 22000, 17500],
                    backgroundColor: 'rgba(21, 137, 238, 0.7)'
                }]
            },
            options: { responsive: true, maintainAspectRatio: false }
        });
    }
}
<!-- chartWidget.html -->
<template>
    <template if:true={error}>
        <p class="slds-text-color_error">{error}</p>
    </template>
    <template if:true={chartInitialized}>
        <div style="height: 300px;">
            <canvas></canvas>
        </div>
    </template>
</template>

loadScript and loadStyle

The lightning/platformResourceLoader module exposes two functions:

Both accept the component instance as their first argument (always this) so the platform can scope and track the injected assets per component. Both are idempotent within a page — if the same URL is requested by multiple component instances, the script/style is only loaded once.

Loading Multiple Resources in Order

Some libraries depend on each other and must load sequentially. Use Promise chaining:

import { LightningElement } from 'lwc';
import { loadScript, loadStyle } from 'lightning/platformResourceLoader';
import jqueryBase from '@salesforce/resourceUrl/jqueryBundle';
import selectizeBase from '@salesforce/resourceUrl/selectizeBundle';

export default class SelectizeWidget extends LightningElement {
    connectedCallback() {
        loadScript(this, jqueryBase + '/jquery.min.js')
            .then(() => loadScript(this, selectizeBase + '/selectize.min.js'))
            .then(() => loadStyle(this, selectizeBase + '/selectize.default.css'))
            .then(() => this.initSelectize())
            .catch(err => console.error('Resource load failed:', err));
    }

    initSelectize() {
        // jQuery and Selectize are now available globally
    }
}

Each .then() waits for the previous load to complete before beginning the next. This guarantees Selectize finds jQuery already defined in the global scope when it executes.

Using Static Resources in Visualforce

For completeness — Visualforce pages reference static resources through the $Resource global merge field.

<!-- Single file resource -->
<apex:includeScript value="{!$Resource.myScript}"/>
<apex:stylesheet value="{!$Resource.myStyles}"/>

<!-- File inside a zip -->
<apex:includeScript value="{!URLFOR($Resource.chartjsBundle, 'chart.umd.min.js')}"/>
<apex:image url="{!URLFOR($Resource.imageBundle, 'icons/logo.png')}" />

URLFOR(resource, path) is the Visualforce equivalent of appending a path to a zip resource URL. The pattern maps directly to PageReference.forResource(name, path) in Apex.

Namespaced Static Resources in Managed Packages

If your component ships inside a managed package with namespace mypkg, the import syntax changes:

import myAsset from '@salesforce/resourceUrl/mypkg__myResourceName';

In subscriber orgs the resource name is prefixed with the namespace and double underscore. In the packaging org itself you reference it without the prefix. Structure your code so the import lives in one place to make namespace swapping a single-line change.

Apex Test Coverage for Static Resource Reads

SOQL against StaticResource works in test context only if the resource exists in the org. For unit tests you should stub the data layer behind a thin interface so the test does not depend on org state.

@IsTest
private class CountryCodeServiceTest {

    @IsTest
    static void testGetCountryNameReturnsLabel() {
        // StaticResource 'CountryCodeMap' must exist in the org for this
        // integration-style test to pass. Use it in full-copy sandboxes.
        // For isolated unit tests, mock StaticResourceReader.readJsonResource.
        Test.startTest();
        String result = CountryCodeService.getCountryName('US');
        Test.stopTest();

        System.assertEquals('United States', result,
            'US ISO code should resolve to United States');
    }

    @IsTest
    static void testGetCountryNameUnknownCode() {
        Test.startTest();
        String result = CountryCodeService.getCountryName('ZZ');
        Test.stopTest();

        System.assertEquals('Unknown', result,
            'Unrecognised code should return Unknown');
    }
}

For unit tests that must run without org data, abstract the SOQL behind a @TestVisible virtual method or a selector pattern so a mock can inject a Blob directly — the same approach used for any sObject dependency.

Static Resources vs. Other File Storage Options

Knowing when not to use Static Resources is just as important as knowing how to use them.

Common Mistakes and How to Avoid Them

Full Working Example — Image Gallery LWC with Zip Resource

This example bundles multiple images in a zip, references each by constructed URL, and renders a responsive gallery.

// Zip structure:
// productImages.zip
// ├── hero.jpg
// ├── thumb1.jpg
// ├── thumb2.jpg
// └── thumb3.jpg

// imageGallery.js
import { LightningElement } from 'lwc';
import productImagesBase from '@salesforce/resourceUrl/productImages';

export default class ImageGallery extends LightningElement {
    images = [
        { id: 1, src: productImagesBase + '/thumb1.jpg', alt: 'Product shot 1' },
        { id: 2, src: productImagesBase + '/thumb2.jpg', alt: 'Product shot 2' },
        { id: 3, src: productImagesBase + '/thumb3.jpg', alt: 'Product shot 3' }
    ];

    heroSrc = productImagesBase + '/hero.jpg';
}
<!-- imageGallery.html -->
<template>
    <figure>
        <img src={heroSrc} alt="Hero product" style="width:100%;" />
    </figure>
    <div class="slds-grid slds-wrap slds-gutters_small">
        <template for:each={images} for:item="img">
            <div key={img.id} class="slds-col slds-size_1-of-3">
                <img src={img.src} alt={img.alt} style="width:100%;" />
            </div>
        </template>
    </div>
</template>

Summary

Static Resources are Salesforce's deployment-managed file store for application assets. Upload single files for standalone scripts, images, or stylesheets; upload zip archives when a library ships with subdirectories or you need to bundle related assets together. In LWC, always use the @salesforce/resourceUrl/ import — it gives you a versioned URL the platform keeps current on every deploy. Load scripts and styles through lightning/platformResourceLoader so the platform manages injection lifecycle; chain promises when libraries have dependencies. In Apex, use PageReference.forResource() for URL generation and query the StaticResource sObject when you need to read file content at runtime — then cache aggressively inside the transaction. Keep Cache Control set to Public for all library and image assets to benefit from CDN caching, and never hard-code versioned URL paths anywhere in your code.

```
← All articles