| name | sfcc-sfra-controllers |
| description | Guide for developing SFRA controllers in Salesforce B2C Commerce. Use this when asked to create controllers, extend base functionality, implement middleware chains, handle routing, or customize storefront behavior. |
SFRA Controllers Skill
This skill guides you through creating storefront controllers for Salesforce B2C Commerce using the Storefront Reference Architecture (SFRA).
Overview
Controllers are JavaScript modules that handle storefront requests. A controller URL has this structure:
https://{domain}/on/demandware.store/Sites-{SiteName}-Site/{locale}/{ControllerName}-{FunctionName}
Example: https://example.com/on/demandware.store/Sites-RefArch-Site/en_US/Home-Show
MVC Pattern
SFRA uses Model-View-Controller pattern:
- Controller (
/controllers): Handles requests, calls models, determines view
- Model (
/models): Business logic, returns JSON objects
- View (
/templates): ISML templates, renders pdict data only
Cartridge Path: Custom cartridges must precede app_storefront_base to override functionality. Never edit the base cartridge directly.
File Location
Controllers reside in the cartridge's controllers directory:
/my-cartridge
/cartridge
/controllers
Home.js # URL: Home-{function}
Product.js # URL: Product-{function}
Cart.js # URL: Cart-{function}
Basic Controller Pattern
'use strict';
var server = require('server');
server.get('Show', function (req, res, next) {
res.render('home/homepage');
next();
});
server.post('Subscribe', function (req, res, next) {
var email = req.form.email;
res.json({ success: true });
next();
});
module.exports = server.exports();
Request & Response Objects
Request (req)
req.querystring
req.form
req.httpMethod
req.httpHeaders
req.currentCustomer
req.locale
req.session
Response (res)
res.render('template', model)
res.json(object)
res.redirect(url)
res.setViewData(data)
res.getViewData()
res.setStatusCode(code)
res.cacheExpiration(hours)
Middleware Chain
Each route is a series of functions with (req, res, next):
server.get('Show',
middleware1,
middleware2,
function (req, res, next) {
next();
}
);
Critical: Always call next() to pass control, unless terminating the request.
Common Built-in Middleware
| Middleware | Purpose |
|---|
server.middleware.https | Require HTTPS connection |
server.middleware.include | Mark as remote include only |
cache.applyDefaultCache | Apply 24-hour page caching |
csrfProtection.generateToken | Generate CSRF token |
csrfProtection.validateRequest | Validate CSRF for POST |
userLoggedIn.validateLoggedIn | Require authenticated user |
consentTracking.consent | Check tracking consent |
Controller Extension
Customize without modifying base code. Always import base first:
'use strict';
var server = require('server');
var page = module.superModule;
server.extend(page);
Extension Methods
| Method | Use Case |
|---|
server.append() | Add/modify data after base logic (most common) |
server.prepend() | Run validation before base logic |
server.replace() | Completely replace route behavior |
Append Example (Most Common)
server.append('Show', function (req, res, next) {
var viewData = res.getViewData();
viewData.customData = 'My custom value';
res.setViewData(viewData);
next();
});
module.exports = server.exports();
Replace Example (State-Changing Actions)
server.replace('Submit', function (req, res, next) {
res.json({ success: true });
next();
});
Critical: Never use append for state-changing routes (payments, inventory). Use replace to prevent duplicate transactions.
CSRF Protection
var csrfProtection = require('*/cartridge/scripts/middleware/csrf');
server.get('ShowForm', csrfProtection.generateToken, function (req, res, next) {
res.render('myForm', { pageTitle: 'Contact' });
next();
});
server.post('Submit', csrfProtection.validateRequest, function (req, res, next) {
next();
});
Important: Middleware automatically adds CSRF to pdict. Never manually add tokens to viewData.
Template usage:
<input type="hidden" name="${pdict.csrf.tokenName}" value="${pdict.csrf.token}"/>
Error Handling
server.get('Show', function (req, res, next) {
try {
var product = ProductMgr.getProduct(req.querystring.pid);
if (!product) {
res.setStatusCode(404);
res.render('error/notfound');
return next();
}
res.render('product/detail', { product: product });
} catch (e) {
Logger.error('Product error: ' + e.message);
res.setStatusCode(500);
res.render('error/general');
}
next();
});
Module Imports
var ProductMgr = require('dw/catalog/ProductMgr');
var Transaction = require('dw/system/Transaction');
var Logger = require('dw/system/Logger');
var URLUtils = require('dw/web/URLUtils');
var collections = require('*/cartridge/scripts/util/collections');
var productHelper = require('*/cartridge/scripts/helpers/productHelpers');
Best Practice: Require modules inside route functions, not at file top.
URL Generation
var URLUtils = require('dw/web/URLUtils');
var productUrl = URLUtils.url('Product-Show', 'pid', 'ABC123');
var loginUrl = URLUtils.https('Login-Show');
var imageUrl = URLUtils.staticURL('/images/logo.png');
Route Events
Execute code at specific points in request lifecycle:
server.get('Show', function (req, res, next) {
res.setViewData({ email: req.form.email });
next();
});
this.on('route:BeforeComplete', function (req, res) {
var viewData = res.getViewData();
});
JSON API Endpoints
server.get('GetCart', function (req, res, next) {
var BasketMgr = require('dw/order/BasketMgr');
var CartModel = require('*/cartridge/models/cart');
var basket = BasketMgr.getCurrentBasket();
var cart = new CartModel(basket);
res.json(cart);
next();
});
Complete Controller Example
'use strict';
var server = require('server');
var cache = require('*/cartridge/scripts/middleware/cache');
var csrfProtection = require('*/cartridge/scripts/middleware/csrf');
var userLoggedIn = require('*/cartridge/scripts/middleware/userLoggedIn');
server.get('Show',
cache.applyDefaultCache,
function (req, res, next) {
var ProductMgr = require('dw/catalog/ProductMgr');
var product = ProductMgr.getProduct(req.querystring.pid);
res.render('product/detail', { product: product });
next();
}
);
server.get('EditProfile',
server.middleware.https,
userLoggedIn.validateLoggedIn,
csrfProtection.generateToken,
function (req, res, next) {
res.render('account/editProfile');
next();
}
);
server.post('SaveProfile',
server.middleware.https,
userLoggedIn.validateLoggedIn,
csrfProtection.validateRequest,
function (req, res, next) {
res.json({ success: true });
next();
}
);
module.exports = server.exports();
Best Practices
- Always call
next() in middleware chain
- Use ViewModels to prepare data for templates
- Keep controllers thin - move logic to scripts/helpers
- Use hooks for functionality that works for storefront and OCAPI
- Handle errors gracefully - never expose stack traces
- Use
*/cartridge/... for portable module paths
- Lazy load modules - require inside functions, not at file top
- Use page caching via middleware or
res.cacheExpiration()
Related Skills
- sfcc-localization - Essential for localizing controller responses. Use
Resource.msg() for all user-facing messages, error strings, and JSON responses. Never hardcode text in controllers.
- sfcc-logging - Comprehensive logging patterns including categories, custom log files, and log level checks for debugging controllers.
Detailed References
For comprehensive patterns and examples: