Loading...

B2C Commerce SFRA Controllers and Middleware: Request Shape

SFRA controllers replace SiteGenesis pipelines. Middleware composes the request lifecycle. The patterns are familiar from Express but the SFCC specifics decide whether your controller is two lines or two hundred.

B2C Commerce SFRA Controllers and Middleware: Request Shape

Key takeaways

  • SFRA controllers look like Express middleware but with SFCC- specific extension patterns (append, prepend, replace) that matter for production code. Getting them wrong produces controllers that appear composable but break when a downstream cartridge tries to extend them.
  • Append adds logic after the base controller runs (most common safe extension). Prepend adds logic before. Replace overrides entirely and is the dangerous choice — every replace requires re-implementing the base behaviour and breaks each time Salesforce ships a base-cartridge update.
  • Middleware composition handles the cross-cutting concerns: authentication checks, CSRF validation, cache settings, locale detection. Production controllers chain 3 to 6 middleware functions before the route handler; understanding the order is what keeps the request lifecycle predictable.
  • Controllers should stay thin. Business logic moves to models and helpers; controllers handle request routing, middleware composition, and response shape only. Controllers that grow past 100 lines almost always have business logic that belongs in a model decorator.

SFRA controllers handle requests in a way that will feel familiar to anyone who has used Express or Connect: route registration, middleware chains, request and response objects. The familiar shape masks B2C Commerce specifics that matter for production code. The extension patterns (append, prepend, replace) are particular to SFRA, not standard middleware. Getting them wrong produces controllers that look composable but actually break when a downstream cartridge tries to extend them.

Sapota's Salesforce team has shipped controllers across SFRA sites of varying sizes. The patterns below are what we apply during code review on B2C Commerce engagements.

The basic controller shape

A simple SFRA controller (cartridges/app_storefront_base/cartridge/controllers/Account.js):

'use strict';

var server = require('server');

server.get('Login', server.middleware.https, function (req, res, next) {
  res.render('account/login');
  next();
});

server.post(
  'Authenticate',
  server.middleware.https,
  function (req, res, next) {
    var formInfo = res.getViewData();
    // ... auth logic ...
    res.json({ success: true });
    next();
  }
);

module.exports = server.exports();

Three things to internalize:

1. server.get and server.post register routes. The first argument is the route name (URL path segment). The platform builds the full URL as /Account-Login from the controller name plus route name.

2. Middleware is a function with (req, res, next) signature. Built-in middleware like server.middleware.https enforces HTTPS. Custom middleware composes additional logic. next() calls the next middleware in the chain.

3. res.render returns a template, res.json returns JSON, res.redirect returns a redirect. The response is set up across middleware; the platform sends it after the last next().

The extension patterns

SFRA's killer feature is the ability for downstream cartridges to extend base controllers without modifying the base. Three methods on server:

server.append. Add behavior after the existing route's middleware chain. Used to enrich the response data or perform side effects after the base logic.

server.prepend. Add behavior before the existing route's middleware chain. Used to set up state or check preconditions before the base logic runs.

server.replace. Replace the existing route entirely. Used when the new behavior is fundamentally different from the base.

A typical custom cartridge extending base account login:

'use strict';

var server = require('server');
server.extend(module.superModule);

server.append('Login', function (req, res, next) {
  // Add a custom analytics event after base login renders.
  var viewData = res.getViewData();
  viewData.customAnalyticsFlag = true;
  res.setViewData(viewData);
  next();
});

module.exports = server.exports();

The pattern: require server, extend module.superModule (which the platform sets to the same-named controller from the cartridge below in the path), append, prepend, or replace as needed.

server.replace is the heavy hammer. Use it only when the base behavior is genuinely wrong for the use case. Most customizations are append or prepend; they preserve the base logic and add to it.

Middleware composition

Middleware functions can be chained on a single route:

server.get(
  'Show',
  server.middleware.https,
  csrfMiddleware.validateRequest,
  cache.applyShortPromotionSensitiveCache,
  function (req, res, next) {
    // Main controller logic.
    res.render('product/productDetails', {
      product: ProductModel.get(req.querystring.pid)
    });
    next();
  }
);

Each middleware function runs in order. Any function can:

  • Set state on req (e.g., req.currentCustomer)
  • Modify the response (e.g., set cookies, add headers)
  • Short-circuit the chain by NOT calling next()
  • Throw an error to skip to the error handler

Built-in middleware to know:

  • server.middleware.https enforces HTTPS; redirects HTTP to HTTPS.
  • server.middleware.include declares the controller as an include-only target (cannot be hit directly via URL).
  • csrfProtection.validateAjax checks CSRF token on AJAX requests.
  • cache.applyDefaultCache and applyShortPromotionSensitiveCache set caching headers.

ViewData and the model pattern

Controllers populate viewData and the template renders it. The flow:

server.get('Show', function (req, res, next) {
  var ProductFactory = require('*/cartridge/scripts/factories/product');
  var productModel = ProductFactory.get({ pid: req.querystring.pid });

  res.render('product/productDetails', {
    product: productModel,
    metaTitle: productModel.name,
    canonicalUrl: URLUtils.url('Product-Show', 'pid', productModel.id).abs()
  });

  next();
});

The res.render call passes the data shape the template expects. The model handles shaping the underlying API objects (product, customer, cart) into template-friendly structures.

Two patterns that matter:

1. Model in factory. The factory pattern (scripts/factories/product) wraps the platform's dw.catalog.ProductMgr.getProduct(pid) and returns a clean model. The controller delegates; the model isolates the API surface from the template.

2. ViewData immutable in the route, mutable in extensions. The base controller sets viewData. Extensions in downstream cartridges read it via res.getViewData(), mutate it, and write it back via res.setViewData(viewData). This pattern allows multiple extensions to compose without conflicts.

Error handling

Controllers can throw or return errors. The platform handles them:

server.get('Show', function (req, res, next) {
  try {
    var productModel = ProductFactory.get({ pid: req.querystring.pid });
    if (!productModel) {
      res.setStatusCode(404);
      res.render('error/notFound');
      return next();
    }
    res.render('product/productDetails', { product: productModel });
  } catch (e) {
    var logger = require('dw/system/Logger');
    logger.error('Product show error: {0}', e.message);
    res.setStatusCode(500);
    res.render('error/serverError');
  }
  next();
});

Three principles:

1. Log every caught exception. With context. Errors silently swallowed produce production incidents with no audit trail.

2. Return specific status codes. 404 for not-found, 400 for validation, 500 for server error. Generic 200 with an error page hurts caching and analytics.

3. Render error templates, not raw stack traces. Users see a branded error page. Stack traces go to logs.

Sapota's controller conventions

The patterns that hold up:

Thin controllers, fat models. Controller responsibilities: route registration, middleware composition, data passing to templates. Business logic lives in models or services. Controllers over 100 lines warrant refactor.

Resource keys for error messages. Same as forms: no hardcoded strings.

Test coverage with the SFCC unit test framework or Mocha against mocked dw modules. Controllers can be tested by mocking req, res, and the dw API objects.

Append over replace. Almost every customization should be append or prepend. Replace is a code review red flag that requires justification.

Documentation of why each middleware is present. A controller with 6 middleware functions in the chain needs a comment explaining why each one is there. Future maintainers (and the original author 6 months later) will thank you.

Common controller mistakes

Five patterns Sapota has seen on engagements:

1. Business logic inline in controller. A 200-line controller computing prices, applying promotions, querying inventory. Move to models and services. Controller orchestrates; models compute.

2. Replace instead of append. Replacing the base Login controller entirely to add one analytics call. The replace loses every base improvement Salesforce ships in future releases. Use append.

3. No error handling. A controller that lets exceptions bubble to the framework's default handler. Generic 500 page, no logs, no diagnosis. Wrap try/catch, log, render specific error templates.

4. Mutating viewData directly without setViewData. A custom append modifies viewData.product.name. The mutation does not persist across the chain. Always setViewData after mutation.

5. Caching headers misapplied. A controller with cache.applyDefaultCache on a customer-specific page. The cached response leaks across users. Use applyShortPromotionSensitiveCache or no cache on customer-personalized content.

What good controller architecture looks like

A B2C Commerce SFRA site with healthy controllers:

  • Thin controllers (typically under 100 lines).
  • Models and services do business logic.
  • Append/prepend for customizations; replace only with justification.
  • Middleware composition documented inline.
  • Error handling specific to the failure type.
  • Caching directives matched to content sensitivity.
  • Test coverage including extension chains.

Sapota's Salesforce team applies these patterns as code review gating criteria on every B2C Commerce engagement. The SFRA controller model is one of the parts of B2C Commerce most easily over-customized; the right discipline keeps the codebase upgradeable.


Building or refactoring SFRA controllers in B2C Commerce? Sapota's Salesforce team, certified on B2C Commerce Developer (Comm-Dev-101), handles controller architecture, middleware composition, and extension strategy on production engagements. Get in touch ->

See our full platform services for the stack we cover.

Contact Us Now

Share Your Story

We build trust by delivering what we promise – the first time and every time!

We'd love to hear your vision. Our IT experts will reach out to you during business hours to discuss making it happen.

WHY CHOOSE US

"Collaborate, Elevate, Celebrate where Associates - Create Project Excellence"

SapotaCorp beyond the IT industry standard, we are

  • Certificated
  • Assured quality
  • Extra maintenance

Tell us about your project