Menu

In this article we will see how to manage the e-commerce checkout flow in ExpressJS.

The checkout process is divided into four phases:

  1. The user authenticates or registers on the site.
  2. The user enters his billing and shipping details.
  3. The user makes the payment.
  4. The user is redirected to the order completion page.

Our routes will require session persistence of data, so for our purpose we will use the NPM module cookie-session. We also need to validate the data entered by the user, so we will use the NPM module validator.

The main file of our application will be the following:

'use strict';

const path = require('path');
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const helmet = require('helmet');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');
const routes = require('./routes');

app.set('view engine', 'ejs');

app.disable('x-powered-by');

app.use('/public', express.static(path.join(__dirname, 'public')));
app.use(helmet());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieSession({
  name: 'name',
  keys: ['key1', 'key2']
}));
app.use('/', routes);


app.listen(port);

The first step provides two forms as a view, one for login and the other for registration.      The section variable is used in the view to set the current navigation menu item.

'use strict';

const express = require('express');
const router = express.Router();
const validator = require('validator');


router.get('/checkout', (req, res, next) => {
    res.render('index', {
       title: 'Checkout',
       section: 'info'
    });
});

module.exports = router;

Here we have to manage the two form POST requests.

router.post('/login', (req, res, next) => {
    const { email, password } = req.body;
    const errors = [];

    if(!validator.isEmail(email)) {
        errors.push({
            param: 'email',
            msg: 'Invalid e-mail address.'
        });
    }

    if(validator.isEmpty(password)) {
        errors.push({
            param: 'password',
            msg: 'Invalid password.'
        });
    }

    if(errors.length) {
        res.json({ errors });
    } else {
        if(!req.session.user) {
            req.session.user = { email };
        }
        res.json({ loggedIn: true });
    }
});

router.post('/register', (req, res, next) => {
    const { name, email, password } = req.body;
    const errors = [];

    if(validator.isEmpty(name)) {
        errors.push({
            param: 'name',
            msg: 'Invalid name.'
        });
    }

    if(!validator.isEmail(email)) {
        errors.push({
            param: 'email',
            msg: 'Invalid e-mail address.'
        });
    }

    if(validator.isEmpty(password)) {
        errors.push({
            param: 'password',
            msg: 'Invalid password.'
        });
    }

    if(errors.length) {
        res.json({ errors });
    } else {
        if(!req.session.user) {
            req.session.user = { name, email };
        }
        res.json({ registered: true });
    }
});

If there are no validation errors, a user object is created in the current session.      Otherwise the errors are passed as an array of JSON objects to the client-side JavaScript code.

Each error object contains the message to be displayed and a reference to the corresponding form element      (attributes id or name).

The second step concerns the insertion of billing and shipping data.      In this route we verify that the user has actually made the first step.      Otherwise we redirect it to the checkout page.

router.get('/billing-shipping', (req, res, next) => {
    if(req.session.user) {
        res.render('billing-shipping', {
            title: 'Billing and shipping',
            section: 'billing',
            user: req.session.user
        });
    } else {
        res.redirect('/checkout');
    }
});

At this point we have to process the POST request of the form in this section.

router.post('/billing-shipping', (req, res, next) => {
    const post = req.body;
    const errors = [];

    if(validator.isEmpty(post.billing_first_name)) {
        errors.push({
            param: 'billing_first_name',
            msg: 'Required field.'
        });
    }
    if(validator.isEmpty(post.billing_last_name)) {
        errors.push({
            param: 'billing_last_name',
            msg: 'Required field.'
        });
    }
    if(!validator.isEmail(post.billing_email)) {
        errors.push({
            param: 'billing_email',
            msg: 'Invalid e-mail address.'
        });
    }

    if(validator.isEmpty(post.billing_address)) {
        errors.push({
            param: 'billing_address',
            msg: 'Required field.'
        });
    }

    if(validator.isEmpty(post.billing_city)) {
        errors.push({
            param: 'billing_city',
            msg: 'Required field.'
        });
    }

    if(!validator.isNumeric(post.billing_zip)) {
        errors.push({
            param: 'billing_zip',
            msg: 'Invalid postal code.'
        });
    }

    if(!post.same_as) {
        if(validator.isEmpty(post.shipping_first_name)) {
            errors.push({
                param: 'shipping_first_name',
                msg: 'Required field.'
            });
        }
        if(validator.isEmpty(post.shipping_last_name)) {
            errors.push({
                param: 'shipping_last_name',
                msg: 'Required field.'
            });
        }
        if(!validator.isEmail(post.shipping_email)) {
            errors.push({
                param: 'shipping_email',
                msg: 'Invalid e-mail address.'
            });
        }
    
        if(validator.isEmpty(post.shipping_address)) {
            errors.push({
                param: 'shipping_address',
                msg: 'Required field.'
            });
        }
    
        if(validator.isEmpty(post.shipping_city)) {
            errors.push({
                param: 'shipping_city',
                msg: 'Required field.'
            });
        }
    
        if(!validator.isNumeric(post.shipping_zip)) {
            errors.push({
                param: 'shipping_zip',
                msg: 'Invalid postal code.'
            });
        }
    }

    if(errors.length > 0) {
        res.json({ errors });
    } else {
        const billing = {};
        

        for(let prop in post) {
            if(prop.startsWith('billing')) {
                let key = prop.replace('billing', '').replace(/_/g, '');
                billing[key] = post[prop];
            }
        }

        req.session.user.billing = billing;

        if(!post.same_as) {
            const shipping = {};

            for(let prop in post) {
                if(prop.startsWith('shipping')) {
                    let key = prop.replace('shipping', '').replace(/_/g, '');
                    shipping[key] = post[prop];
                }
            }

            req.session.user.shipping = shipping;
        }

        res.json({ saved: true });
    }
});

If there are no validation errors, the entered data will be added as properties of the user object saved in the session.

The third step is payment. Here you can enter the form that creates the transaction with the remote gateway (in our example it's PayPal).      In this route we must always check that the user has completed the previous steps.

router.get('/payment', (req, res, next) => {
    if(!req.session.user) {
        res.redirect('/checkout');
        return;
    }

    const { user } = req.session;

    if(!user.billing) {
        res.redirect('/billing-shipping');
        return;
    }

    res.render('payment', {
        title: 'Payment',
        section: 'payment',
        user
    });
});

The final step takes place when the user is redirected to the order completion page after making the payment.

router.get('/thank-you', (req, res, next) => {
    if(req.session.user && req.session.user.billing) {
        res.render('thank-you', {
            title: 'Order complete',
            section: 'thank-you',
            user: req.session.user
        });
    } else {
        res.redirect('/checkout');
    }
});

At this point in a real e-commerce the current session should be reset      and sent a confirmation email to the user with an order summary.

Demo

Heroku

Source code

GitHub

Share: