sfcc-localserviceregistry — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sfcc-localserviceregistry (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This guide provides a concise overview of best practices for creating server-to-server integrations in Salesforce B2C Commerce Cloud using the dw.svc.LocalServiceRegistry.
Integrations use a two-part architecture:
Declarative Configuration (Business Manager): Defines the what, who, and how of the service call. This includes the endpoint, credentials, and operational policies like timeouts and circuit breakers.
Programmatic Definition (Script Module): Defines the dynamic behavior using callbacks. This includes creating the request payload, parsing the response, and defining mock behavior for testing.
| Component | Purpose | Key Settings |
|---|---|---|
| Credential | Stores endpoint URL and authentication details. | ID, URL, User, Password |
| Profile | Defines operational behavior and resilience. | Timeout, Rate Limiting, Circuit Breaker |
| Service | Binds a Credential and Profile to a named service ID. | Name (ID), Service Type, Mode (Live/Mocked) |
Best Practice: Use a period-delimited naming convention for the Service ID (e.g., int_myapi.http.customer.get) to organize logs effectively.
The modern approach uses dw.svc.LocalServiceRegistry to define service behavior locally, eliminating the need for global initialization scripts.
The createService method takes a configuration object with the following core callback functions:
dw.net.HTTPClient). It should parse the response body (e.g., JSON.parse(client.text)) and throw an Error if the status code indicates a failure (e.g., client.statusCode >= 400). This ensures the result.ok flag is set correctly.createRequest and parseResponse still run. Ideal for integration testing your service's logic.dw.svc.Result (What service.call() Returns)The Service Framework returns a dw.svc.Result object. Your calling code should branch on result.ok and also differentiate transport/config failures from HTTP/API failures.
| Field | Meaning |
|---|---|
ok | true if the call succeeded and parseResponse did not error |
status | Typically OK, ERROR, or SERVICE_UNAVAILABLE |
object | Parsed value returned by parseResponse |
error / errorMessage | Error code/message (often HTTP status + message) |
unavailableReason | Why the framework refused to call (timeout, rate limit, circuit breaker, disabled, config issue) |
mockResult | Whether the response came from mock mode |
Practical rule: treat SERVICE_UNAVAILABLE as a resilience signal (retry/fallback), and treat ERROR as a downstream/API problem (inspect HTTP status and body).
Encapsulate all logic for an integration into a single script module. Use a singleton pattern to avoid re-creating the service definition on every call.
'use strict';
var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');
var Logger = require('dw/system/Logger');
// --- Private Helper for Operation-Specific Details ---
/**
* Returns configuration details for a specific service operation.
* @param {string} operation - The operation ID (e.g., 'getCustomer')
* @returns {{method: string, path: string}}
*/
function getOperationDetails(operation) {
switch (operation) {
case 'getCustomer':
return { method: 'GET', path: '/customers/' };
case 'createCustomer':
return { method: 'POST', path: '/customers' };
default:
Logger.error('Unknown operation in MyAPIService: {0}', operation);
throw new Error('Operation not implemented: ' + operation);
}
}
// --- Service Definition ---
var myAPIService = LocalServiceRegistry.createService('int_myapi.http.customer', {
/**
* @param {dw.svc.HTTPService} svc
* @param {string} operation - The name of the operation to perform
* @param {Object} params - The parameters for the operation
* @returns {string|null} - Request body or null
*/
createRequest: function (svc, operation, params) {
var details = getOperationDetails(operation);
var credential = svc.getConfiguration().getCredential();
svc.setRequestMethod(details.method);
svc.addHeader('Content-Type', 'application/json');
svc.addHeader('X-API-Key', 'your-api-key'); // Example custom header
var url = credential.getURL() + details.path;
if (details.method === 'GET' && params && params.id) {
svc.setURL(url + params.id);
return null; // No body for GET
}
svc.setURL(url);
return params? JSON.stringify(params) : null;
},
/**
* @param {dw.svc.HTTPService} svc
* @param {dw.net.HTTPClient} client
* @returns {Object} - The parsed JSON response
*/
parseResponse: function (svc, client) {
if (client.statusCode >= 400) {
Logger.error('MyAPIService error: {0} {1} - {2}', client.statusCode, client.statusMessage, client.text);
throw new Error('API call failed with status ' + client.statusCode);
}
try {
return JSON.parse(client.text);
} catch (e) {
Logger.error('Error parsing JSON response from MyAPIService: {0}', client.text);
throw new Error('Invalid JSON response');
}
},
/**
* @param {dw.svc.HTTPService} svc
* @param {string} operation
* @param {Object} params
* @returns {Object} - The final, parsed mock object
*/
mockFull: function (svc, operation, params) {
var details = getOperationDetails(operation);
if (details.method === 'GET') {
return { id: params.id, name: 'Mock Customer', email: '[email protected]' };
}
if (details.method === 'POST') {
return { id: 'mock-' + new Date().getTime(), name: params.name, email: params.email };
}
return { error: 'Mock not implemented for ' + operation };
},
/**
* Redacts sensitive data from logs.
* @param {string} msg - The log message
* @returns {string} - The filtered message
*/
filterLogMessage: function (msg) {
// Basic redaction example for JSON strings
try {
var logObject = JSON.parse(msg);
if (logObject.password) {
logObject.password = '<REDACTED>';
}
return JSON.stringify(logObject);
} catch (e) {
return msg; // Not a JSON message, return as is
}
}
});
## 2.2 Production Safety Notes
- Prefer `filterLogMessage` for any integration that may include secrets, tokens, passwords, or PII.
- Keep `parseResponse` strict: if `client.statusCode >= 400`, throw an error so failures propagate through `result.ok === false`.
- Use Business Manager service profiles for timeouts, rate limits, and circuit breaker behavior.
- For comprehensive logging patterns including categories, custom log files, and log level checks, see **[sfcc-logging](../sfcc-logging/SKILL.md)**.
## 2.3 HTTP Response Caching (Service Framework)
For third-party APIs with stable responses (for short periods), you can enable response caching at the HTTP client level inside your service definition.
### Enabling caching
Enable caching inside `createRequest` (or later callbacks), after the client is initialized:
createRequest: function (svc, operation, params) { // Only access svc.client inside createRequest/parseResponse/etc. // NOTE: TTL units depend on the underlying API you call; validate against your SFCC version docs. svc.client.enableCaching(1000);
svc.setRequestMethod('GET'); // ... set URL/headers/body ... return null; }
### Clearing the cache
You can clear the HTTP client response cache in Business Manager:
- **Administration → Operations → Service Maintenance**
- Use the **Invalidate** action for **HTTP Client Response Cache**
Constraints to design around:
- Cache invalidation is typically **global** (not per-service).
- Avoid caching error responses.
- Cached calls can still affect service-level statistics (rate limiting/circuit breaker) depending on configuration.
For broader cache strategy (page cache vs custom caches vs service caching), see **[sfcc-caching](../sfcc-caching/SKILL.md)**.
// --- Public API ---
module.exports = {
OPERATION_GET_CUSTOMER: 'getCustomer',
OPERATION_CREATE_CUSTOMER: 'createCustomer',
call: function (operation, params) {
return myAPIService.call(operation, params);
}
};// In controller/MyController.js
var server = require('server');
var MyAPIService = require('~/cartridge/scripts/services/MyAPIService');
server.get('ShowCustomer', function(req, res, next) {
var customerId = req.querystring.id;
var result = MyAPIService.call(MyAPIService.OPERATION_GET_CUSTOMER, { id: customerId });
if (result.ok) {
res.render('customer/customerDetails', {
customer: result.object
});
} else {
res.render('error', {
message: 'Could not retrieve customer data.',
error: result.errorMessage
});
}
next();
});// In controller/MyController.js
var server = require('server');
var MyAPIService = require('~/cartridge/scripts/services/MyAPIService');
server.post('CreateCustomer', function(req, res, next) {
var customerData = {
name: req.form.name,
email: req.form.email
};
var result = MyAPIService.call(MyAPIService.OPERATION_CREATE_CUSTOMER, customerData);
if (result.ok) {
res.json({ success: true, customer: result.object });
} else {
res.setStatusCode(500);
res.json({ success: false, error: result.errorMessage });
}
next();
});Use a Two-Service Pattern for efficiency: one service to get the token, and another to call the API.
dw.system.CacheMgr) to avoid re-authenticating on every call.Authorization: Bearer <token> header in its createRequest callback.// Conceptual snippet for API Service createRequest with OAuth
createRequest: function (svc, params) {
var CacheMgr = require('dw/system/CacheMgr');
var AuthTokenService = require('~/cartridge/scripts/services/AuthTokenService');
// Get token from cache or fetch a new one
var tokenCache = CacheMgr.getCache('MyAPIToken');
var token = tokenCache.get('access_token', function () {
var result = AuthTokenService.call();
if (result.ok && result.object.access_token) {
// Set cache expiry to slightly less than the token's actual expiry
tokenCache.put('access_token', result.object.access_token, result.object.expires_in - 300);
return result.object.access_token;
}
return null;
});
if (!token) {
throw new Error('Unable to retrieve valid API token.');
}
svc.setRequestMethod('GET');
svc.addHeader('Authorization', 'Bearer ' + token);
//... set URL and other request details...
return null;
}This guide walks through the three essential parts of setting up a new service in the Business Manager: creating the Credential, configuring the Profile, and defining the Service itself.
The Service Credential securely stores the endpoint URL and authentication details for the external system.
your.service.name.http.credentials. This name cannot contain spaces.https://api.example.com/v2/.The Service Profile defines the operational behavior, such as timeouts and resilience patterns, for the service call.
default-api-profile, realtime-payment-profile).10000 for 10 seconds). This is the maximum time B2C Commerce will wait for a response before the call fails.10).60000 for 1 minute). When the circuit is tripped, B2C Commerce will stop making calls to the service for a period to allow the external system to recover.1000).60000 for 1 minute).The Service Definition links the Credential and Profile to create the final, named service that you will call from your code.
LocalServiceRegistry.createService() call.Best Practice: Use a period-delimited pattern like {cartridge}.{protocol}.{service}.{operation} (e.g., int_myapi.http.customer.get). This structure automatically organizes your service logs into a helpful hierarchy.
mockCall or mockFull function in your script instead of making a network request.myapi) to create a dedicated log file for this service (service-myapi-....log), which simplifies debugging.filterLogMessage callback in your script to redact sensitive information from these logs.Your service is now fully configured in the Business Manager and ready to be implemented and called from your SFRA cartridge code.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.