SFRA Architecture

SFRA provides an app_storefront_base cartridge and a server module. A storefront site uses the SFRA base cartridge and overlay plugin, LINK, and custom cartridge functionality to create a cartridge stack. Define the order of the cartridge stack by configuring the cartridge path in Business Manager.

The base cartridge contains only the functionality common to most sites. You can layer functionality over the base cartridge with plug-in cartridges, LINK cartridges, and custom code cartridges. B2C Commerce provides plug-in cartridges that provide other features, such as gift registries, Apple Pay, product comparisons, and middleware capability. LINK partners, such as PayPal and Bazaarvoice, provide LINK cartridges for third-party integrations. You can create one or more custom cartridges to override portions of the base cartridge and customize the functionality and branding of your storefront site.

The app_storefront_base cartridge includes multiple models These models use the B2C Commerce script API to retrieve data from the platform for a functional area of the application, such as orders. The models then construct a JSON object that you can use to render a template.

The server module provides objects containing data from HTTP requests, responses, and session objects. The server module registers routes that map a URL to code that's executed when B2C Commerce detects the URL. It uses a modern JavaScript approach that is conceptually similar to NodeJS's Express.

The app_storefront_base cartridge models and server module objects are guaranteed to be backward compatible between major point releases. The guarantee lets you adopt new features more easily and maintains a clear distinction between base and custom code, which can help you troubleshoot issues and adopt bug fixes.

Important: Editing the app_storefront_base cartridge or server module voids the guarantee of backward compatibility and hinders feature and fix adoption. Instead, use B2C Commerce script methods to extend base cartridge functionality. The JSON objects created by the server module and the app_storefront_base models retain their structure and don't change properties between point releases. However, Commerce Cloud Engineering reserves the right to change how these objects are created.

A typical cartridge stack includes several layers, as shown in this table:

Layer Description Tips
Custom Adds specific customizations for your brand and organization. Perform all customizations of the base, LINK, and product cartridges in custom cartridges for easy adoption of future features. Rename all custom cartridges with app_custom_* to make them easy to distinguish.
LINK Adds third-party functionality to your site. You can integrate features from LINK partners, such as payment providers and tax services. You can import LINK partner data, such as tax tables or inventory feeds. See specific LINK cartridge documentation for more information.
Plug-in Enhances the ecommerce capabilities provided by Commerce Cloud or anyone else in the Salesforce community. Cartridges provided by Commerce Cloud let you integrate (optional) products and features such as product compare and gift registry. Plug-ins can create custom objects or data that are specific to a product or feature.
Base Core functionality modified only by the Commerce Cloud team or through contributions to GitHub. The core cartridge includes best-practice code for features used by most customers. In addition to the default features, the base cartridge contains features that can be configured in Business Manager. Some of the features in the base cartridge are configured in Business Manager, such as pick up in store.

The following graphic shows a typical cartridge stack:



The cartridge path is always searched left to right, and the first controller or pipeline found with a particular name is used. This behavior allows cartridges earlier on the path to override the functionality of cartridges later on the path. The cartridge path for this stack is:

app_custom_mybrand:app_custom_mysite:LINK_bazaarvoice:LINK_wordpress:plugin_applepay:plugin_comparison:app_storefront_base

B2C Commerce provides demo data that lets you view and explore the base cartridge as a working site.

Note: Though cartridge data is imported into B2C Commerce and used by the code in each cartridge, it isn't stored in the cartridge itself.

Base Cartridge Architecture

We highly recommend that you do not modify app_storefront_base. Instead, create your own cartridge and add it as an overlay in the Business Manager cartridge path. Using an overlay cartridge lets you upgrade to a newer version of SFRA without having to manually cherry-pick changes and perform manual merges. You can still adjust your custom code, but the upgrade and feature adoption process is quicker and less painful.

storefront-reference-architecture
  dw.json //used to upload code
  package.json
  cartridges
    β”œβ”€β”€ app_storefront_base
        β”œβ”€β”€ client //client-side JavaScript and CSS
        β”‚   β”œβ”€β”€ js
        β”‚   └── scss
        β”œβ”€β”€ controllers // business logic for the application
        β”œβ”€β”€ forms 
        β”œβ”€β”€ models //gets data from server and provides it as JSON objects
        β”œβ”€β”€ scripts //reusable functionality 
        β”‚   β”œβ”€β”€ cart
        β”‚   β”œβ”€β”€ factories
        β”‚   β”œβ”€β”€ helpers
        β”‚   β”œβ”€β”€ payment
        β”‚   └── search
        β”œβ”€β”€ static //static resources unlikely to change, such as branding images
        └── templates //ISML templates
            β”œβ”€β”€ default
            └── resources
                 ...

    β”œβ”€β”€ modules
        └── server
              ...

The Modules Folder and the Server Module

A server module in the SFRA global modules folder provides the routing functionality for controllers. Every SFRA controller requires this module. Any JavaScript module included in the modules folder is globally available and can be required without a path in any controller or script.

The following example requires the server module in the modules folder.

var server = require('server');

The modules folder is a separate cartridge, so you can easily upload it to the platform. However, you don't have to include it on the cartridge path. For information on using the server module, see Extending the Base Cartridge Architecture.

CAUTION:

Don't directly extend or customize the server module. If you customize it directly, B2C Commerce can't be held responsible for changes that are not backward compatible in future versions of SFRA.

However, if you want to extend or customize server module functionality, you can create your own module and place it in the modules folder. You can require server module functions and extend them in your own module as you would any JavaScript function.

ViewModels and Controllers

SFRA uses a variant of Model-View-Controller architecture. Controllers handle information from the user, create ViewModels, and render pages. The ViewModels request data from B2C Commerce, convert B2C Commerce script API objects into pure JSON objects, and apply business logic.

A controller requests data from B2C Commerce and passes the returned objects to a ViewModel to be converted into a serializable JavaScript object.

A ViewModel differs from a standard B2C Commerce script object in the following ways.
  • A B2C Commerce script object sometimes behaves like a Java object, not a JavaScript object, whereas a model is a serializable JavaScript object.
  • A ViewModel provides the data to render pages in the application and often combines data from multiple B2C Commerce script objects.
Note: ViewModels are also referred to simply as models, because that's the name of the folder where they are located in the application.

Defining Endpoints

SFRA defines endpoints (URIs) with a Controller-RouteName syntax. Defining an endpoint depends on your controller's filename and the routes defined within it.

The syntax of your endpoint depends on the SEO options you choose for your site. When developing, you can use the Commerce Cloud Standard URL Syntax without SEO options. This approach makes it easy to test your controllers.

Example: Defining an Endpoint for Your Home Page

Suppose that you want to create an endpoint in B2C Commerce standard URL syntax that looks like the following:
http://www.mystore.com/on/demandware.store/​Sites-YourShopHere-Site/​EN_US/Home-Show

You create a Home.js module in the controller folder with the following code:

'use strict';

var server = require('server');    //the server module is used by all controllers
var cache = require('*/cartridge/scripts/middleware/cache');

server.get('Show', cache.applyDefaultCache, function (req, res, next) {  //registers the Show route for the Home module
    res.render('/home/homepage');      //renders the hompage template
    next();            //notifies middleware chain that it can move to the next step or terminate if this is the last step.
});

module.exports = server.exports();
The Home-Show route consists of the module name (Home.js) and the first parameter in the server.get function (Show).

Defining Routes

The server module use function takes the RouteName of the function to execute as the first argument. SFRA provides two utility methods to call the use function (server.get and server.post) that make sure the URI is either a GET or POST request.

This functionality is different from many other frameworks, which define routes by overriding anchor functionality in the URI to define actions to execute.

How Controllers Are Executed

When a request is made via a URI in the web browser, the last part of the endpoint specifies the Controller-RouteName to execute. In this example, the filename is Page.js and the route name is Show. The Page.js file must be stored in the controllers folder for B2C Commerce to recognize it as a controller.
http://www.mystore.com/on/demandware.store/Sites-YourShopHere-Site/EN_US/Page-Show

The B2C Commerce server executes the first controller in the cartridge path with the correct name. B2C Commerce recognizes both SiteGenesis JavaScript Controller (SGJC) and SFRA controllers as equal and doesn't prioritize one over the other. If no controller is found in the cartridge path, the server executes the first pipeline file found in the cartridge path. Because SFRA doesn't use pipelines, this pipeline is the first one found in a custom cartridge.

When the controller is located, all require statements at the beginning of the controller are executed. These statements must include a require for the server module. The server module is located in the modules folder, which is a peer of the app_storefront_base cartridge. Requiring the server module returns an empty server object.

The next line to be executed is the following:
module.exports = server.exports();

This line is present in all controllers. Calling server.exports() causes the server to register all functions in the controller that use the server.get, server.post, or server.use functions as routes. The server then executes the function whose first parameter matches the route name in the URI.

For example, if you assume the URI in the previous example that ends in Page-Show, B2C Commerce registers all functions in Page.js and then executes the Show function.

server.get('Show', locale, function (req, res, next) {
    res.render('/home/homepage');
    next();
});

Rendering a JSON String

Suppose that this code is saved to a file named Page.js.
var server = require('server');
server.get('Show', function(req, res, next) {
    res.json({ value: 'Hello World'});
    next();
});
module.exports = server.exports();
The Page.js file creates a route for a URL.
http://sandbox-host-name/on/demandware.store/site-name/en_US/Page-Show
Whenever that URL is called, the provided function is executed and renders a page with the following Content-Type header:
Content-Type: application/json
The rendered page also includes the following body:
{ value: 'Hello World '}

Protecting Route Access

You can enhance this code by adding the server.middleware.https parameter after Show, to limit this route to only allow HTTPS requests. This example restricts the Account-Show route to HTTPS.

Note: This function is one of the middleware functions provided by Commerce Cloud.
server.get('Show', server.middleware.https, function (req, res, next) {
    var accountModel = getModel(req);
    if (accountModel) {
        res.render('account/accountdashboard', {
            account: accountModel,
            accountlanding: true
        });
    } else {
        res.redirect(URLUtils.url('Login-Show'));
    }
    next();
});
Note: Information about middleware filtering classes is available in the SFRA JSDoc in the server-side global documentation for the server module.

Using server.use, server.get, or server.post

For a server.get or server.post function, the first parameter is always the name of the route (the URL endpoint). The last parameter is always the main function for the endpoint. Usually, the main function in the controller renders a page for the storefront or redirects to another controller.

You can add as many parameters in between the first and last parameter as you want. Each parameter specifies a function to be executed in order and can let the next step be executed (by calling next() or reject a request by calling next(new Error()).

Example 1: Conditionally Executing a Middleware Step

This example shows a main function that conditionally executes next()or next(new Error()) depending on whether an Apple Pay order is being placed.

server.post('Submit', function (req, res, next) {
    var order = OrderMgr.getOrder(req.querystring.order_id);

    if (!order && req.querystring.order_token !== order.getOrderToken()) {
        return next(new Error('Order token does not match'));
    }

    var orderPlacementStatus = orderHelpers.placeOrder(order);

    if (orderPlacementStatus.error) {
        return next(new Error('Could not place order'));
    }

    var orderModel = orderHelpers.buildOrderModel(order);
    res.render('checkout/confirmation/confirmation', { order: orderModel });
    return next();
});

The code executed between the first and last parameter is referred to as middleware and the entire process is called chaining. You can create middleware functions to limit route access, add information to the data object passed to the template for rendering, or for any other purpose. One limitation to this approach is that you must call the next function at the end of every step in the chain. Otherwise, the next function in the chain is not executed.

Middleware

Each step of a middleware chain is a function that takes three arguments: req, res, and next, in that order.

req

This argument is short for Request. It contains information about the server request that initiated execution. The req object contains user input information, such as the content-type that the user accepts, the user's login and locale information, or session information. The req argument parses query string parameters and assigns them to the req.querystring object.

res

This argument is short for Response. It contains functionality for outputting data back to the client. For example:

  • res.cacheExpiration(24): Sets cache expiration to 24 hours from now.
  • res.render(templateName, data): Outputs an ISML template back to the client and assigns data to pdict.
  • res.json(data): Prints a JSON object back to the screen. It's helpful in creating AJAX service endpoints that you want to execute from the client-side scripts.
  • res.setViewData(data): Doesn't render anything, but sets the output object. This behavior can be helpful if you want to add multiple objects to the pdict of the template. The pdict contains the information for rendering that is passed to the template. setViewData merges all the data that you passed into a single object, so you can call it at every step of the middleware chain. For example, you can create a separate middleware function that retrieves information about a user's locale to render a language switch on the page. The output object of the ISML template or JSON is set after every step of the middleware chain is complete.

    You can also use the ViewData object to extend the data created in a controller that you are extending. You don't have to duplicate the logic used in the original controller to get the data. You only have to add the additional data to the ViewData object and render it.

next()

Executing the next function notifies the server that you are done with a middleware step so that it can execute the next step in the chain.

By chaining multiple middleware functions, you can compartmentalize your code and extend or modify routes without having to rewrite them.

Event Emitters

The server module emits events at every step of execution and you can subscribe and unsubscribe to events from a given route. Use an event emitter to override the middleware chain by removing the event listener and creating a new one. However, if you have to change individual steps in a middleware chain, we recommend that you replace a route. While SFRA does supply removeListener and removeAllListener functions, they don't recognize named event emitters. For this reason, it isn't possible to use Step event emitters to override a specific step in the middleware chain.

The following is a list of currently supported events:

  • route:BeforeComplete is emitted before the route:Complete event but after all middleware functions. Used to store user submitted data to the database; most commonly in forms.
  • route:Complete is emitted after all steps in the chain finish execution. Subscribed to by the server to render ISML or JSON back to the client.
  • route:Redirect is emitted before res.redirect execution.
  • route:Start is emitted as before middleware chain execution.
  • route:Step is emitted before execution of each step in the middleware chain.

All events provide both req and res as parameters to all handlers.

Subscribing or unsubscribing to an event lets you do complex and interesting things. For example, the server subscribes to the route:Complete event to render ISML back to the client. If you want to use something other than ISML to render the content of your template, you can unsubscribe from the route:Complete event. You can subscribe to it again with a function that uses your own rendering engine instead of ISML, without modifying any of the existing controllers.

OnRequest and OnSession Event Handlers

The OnRequest and OnSession event handlers that were implemented as pipelines in SiteGenesis Pipeline Processor (SGPP) and as controllers in SGJC are not used in SFRA. You still have access to request and session data using the middleware req (request) and res (response) objects. However, SFRA avoids using OnRequest and OnSession anywhere in our code outside of the server module.

If you want to implement OnRequest and OnSession, they must be implemented through hooks. B2C Commerce looks for OnSession as the controller name, but the new architecture doesn't do that. The only difference between a hook and controller is that the hook doesn't have access to the req and res objects.

Extending Base Cartridge Architecture

B2C Commerce owns and maintains the app_storefront_base cartridge. The base cartridge is hosted on GitHub and anyone can contribute to it through a pull request. We test all pull requests before they are approved.

The app_storefront_base cartridge contains controllers for business logic, models with JSON objects populated from the B2C Commerce script API, and ISML templates. It also contains the modules directory with a server module for SFRA.
Note: Anything in the modules folder or TopLevel package is globally available and can be required without a path. You can add your own custom modules to the modules folder.

Extension example Plugin_applepay

We provide a sample plugin cartridge as part of the SFRA project that demonstrates how to selectively add custom functionality to the base cartridge. Add this cartridge to the left of the base cartridge on the cartridge path to observe the functionality.

We publish an npm node named sgmf-scripts. This node has tools to compile the CSS and scripts for your storefront site. Use the tools to create a CSS and client-side JavaScript that includes functionality from other cartridges.

Best Practices for Storefront Reference Architecture Maintenance

Make sure to regularly update your app_cartridge_base and server module to have access to the most up-to-date security and bug fixes. Regularly updating your base cartridge.