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:
Public (CDN-cached, fastest) or Private (per-session, for sensitive assets).Public for libraries and images that are not user-specific. Use Private for anything tied to a user session.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.
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
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.
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.
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).
// 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.
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.
// 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.
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:
loadScript(component, url) — injects a <script> tag into the DOM and returns a Promise that resolves when the script executes.loadStyle(component, url) — injects a <link rel="stylesheet"> tag and returns a Promise.
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.
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.
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.
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.
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.
Knowing when not to use Static Resources is just as important as knowing how to use them.
/resource/TIMESTAMP/NAME URL from
your browser and paste it into code. The timestamp changes on every deployment and will break your reference.
Always use @salesforce/resourceUrl/ in LWC or PageReference.forResource() in Apex.
chartjsBase + '/chart.umd.min.js'. A double slash or missing slash produces a 404.
loadScript inside renderedCallback without a guard.
renderedCallback fires every time the component re-renders. Add a boolean flag
(this.scriptsLoaded) and check it before calling loadScript to prevent repeated
injections.
Private cache control on public libraries. Setting cache control to
Private on a CDN-safe asset like Chart.js prevents edge-caching and increases load time for every
user. Reserve Private for assets that contain org-specific or user-specific information.
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>
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.