E-Commerce for Node.js Developers [With Koa.js Tutorial]

In a rush? Skip to technical tutorial or live demo.

We've spent a lot of time lately blogging about frontend JavaScript frameworks.

Vue.js, React, Angular... we <3 these.

I thought I'd shake things up for my first post on the blog, and explore the server-side of JS.

JS-spectrum

Okay, it's not THAT distressing of a ride.

This State of Node.js article from a few months ago was a good introduction, but today I'm going to focus on Node.js for e-commerce.

I'll first expose what Node can bring to your online store and the ecosystem's e-commerce tools.

Then I'll craft my own demo shop using the neat Node.js framework that is Koa.js. Steps:

  1. Initializing the Koa.js app directory.
  2. Creating the app's entry point.
  3. Reading products data.
  4. Setting up Koa.js routes.
  5. Enabling e-commerce capabilities on your Node.js app

Ready for this?

Why use Node.js for e-commerce?

node-js

Node.js is a JavaScript runtime built on Chrome's V8 JS engine. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.

A few of its features make it an excellent choice for your next e-commerce project:

It's JavaScript, and JavaScript is everywhere.

If you ever want to use one of the many popular JS frameworks for your store's frontend, a Node.js backend makes it easy to find code universality across your stack. Plus, it's widely used for server-side rendering to solve JavaScript single page apps SEO issues.

Going with a JavaScript full stack, you can write an e-commerce web app that renders both on the browser and the server seamlessly.

It scales when your business needs it.

You're totally in charge of your Node.js backend configuration. Whatever functionalities you need for a store's backend, you select and add the necessary modules. On this matter, you shouldn't be scared to miss any piece as npm is the widest software registry out there.

It's more popular than ever.

If you're a business owner, you'll never have any problem filling your development team with resourceful Node.js developers.

If you're a single developer working on a small e-commerce client project, you'll find all the help you need from the vast Node.js community.

And then, there's the wide choice of tools available.

Node.js e-commerce tools

There are quite a lot of noteworthy e-commerce solutions in the Node.js ecosystem.

  • Total.js' Eshop Total.js is a full-featured Node.js framework with its built-in e-commerce solution. It can be a good option if you're ready to adopt the framework as a whole.

  • Reaction Commerce An open-source, real-time e-commerce platform that plays nicely with React. It's mostly based on Meteor though, advanced knowledge of the framework is needed here if you want to customize it. (Gotta admit, that website's design is dope!)

  • Prime Fusion An Express.js e-commerce platform built on the MEAN stack (Mongo, Express, Angular, Node). It resides behind an API layer which means you can attach it to any frontend framework for templating and theming. It's open-source, but at the same time maintained by a team providing regular updates.

  • Cezerin Cezerin is a React & Node.js based e-commerce platform for progressive web apps.

  • Spurt Commerce This one offers a Node.js e-commerce backend as well as Angular storefronts. It's still in its first version though and doesn't have a whole lot of built-in features.

As you can see, most of the existing e-commerce solutions are dependant on Node.js frameworks.

In the following example, I'll use Snipcart, which you can integrate within any Node.js e-commerce setup, and the Node framework Koa.js.

Koa.js + Snipcart e-commerce example

There are many great Node.js frameworks I could've tried here. We've already played with Express, but there's Meteor, Sails.js, Nest, Hapi, Strapi & many others.

Koa.js is described as the future of Node.js, so you might understand why I got curious!

koa-js

It was built by the same team behind Express in 2013, the difference being that it's a smaller, more expressive, and more robust foundation for web applications and APIs.

The least I can say about it is that it's minimalistic. I mean, for real.

To prove it, here's my demo use case:

Your friend Lisa is launching a new podcast, and she needs external financing to help her get started. Among other things, she wants a fundraiser website where people can donate by either buying products or give the amount they want.

The specs for this project are:

  • It has to be live quick.
  • No need for a CMS to manage products.

Your goal for this project is to put the minimum online for your friend to get going, in record time.

Koa.js documentation is a one-pager; need I say more? It's dead simple and embraces the "pick the tools you need" philosophy, which makes it a good fit for this project.

You'll be selling stuff, so Snipcart's zero friction setup will serve you well.

Technical tutorial: Node.js e-commerce with Koa.js

node-js-koa-ecommerce

Pre-requisites

1. Initializing the Koa.js app directory

Let's get started by creating your project's directory:

mkdir snipcart-koajs
cd snipcart-koajs

Generate a package.json file with the following content:

{
  "name": "snipcart-koajs",
  "version": "1.0.0",
  "description": "Minimalistic/low-ceremony ecommerce store built on Koa.js using Snipcart",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "dependencies": {
    "config": "^1.30.0",
    "fs-extra": "^6.0.1",
    "koa": "^2.5.2",
    "koa-router": "^7.4.0",
    "koa-static": "^5.0.0",
    "koa-views": "^6.1.4",
    "pug": "^2.0.3"
  },
  "devDependencies": {
    "nodemon": "^1.18.1"
  }
}

1.2 Installing Koa.js dependencies

npm install --save koa koa-router koa-static koa-views pug config fs-extra
npm install --save-dev nodemon

A quick overview of these packages:

  • koa : The core Koa.js framework used to run the web app.
  • koa-router : Maps URL patterns to handler functions.
  • koa-static : Serves static files (stylesheets, scripts).
  • koa-views : Enhances Koa's context object, allowing you to render views using the templating engine of your choice.
  • pug : The templating engine I'll use in this demo.
  • fs-extra : Provides promise support for methods of the native fs module (more on that later).
  • config : I like to use this package to centralize configuration keys.
  • nodemon : When in development, this package watches your files and restarts the app when changes are detected.

2. Creating the app's entry point

Out of the box, Koa is nothing more than a middleware pipeline. You'll have to build on top of that.

The app code will be placed in a file named index.js at root:

//index.js

const config = require('config')
const path = require('path')
const Koa = require('koa')
const Router = require('koa-router')
const loadRoutes = require("./app/routes")
const DataLoader = require('./app/dataLoader')
const views = require('koa-views')
const serve = require('koa-static')

const app = new Koa()
const router = new Router()

// Data loader for products (reads JSON files)
const productsLoader = new DataLoader(
  path.join(
    __dirname,
    config.get('data.path'),
    'products')
)

// Views setup, adds render() function to ctx object
app.use(views(
  path.join(__dirname, config.get('views.path')),
  config.get('views.options')
))

// Serve static files (scripts, css, images)
app.use(serve(config.get('static.path')))

// Hydrate ctx.state with global settings, so they are available in views
app.use(async (ctx, next) => {
  ctx.state.settings = config.get('settings')
  ctx.state.urlWithoutQuery = ctx.origin + ctx.path
  await next() // Pass control to the next middleware
})

// Configure router
loadRoutes(router, productsLoader)
app.use(router.routes())

// Start the app
const port = process.env.PORT || config.get('server.port')
app.listen(port, () => { console.log(`Application started - listening on port ${port}`) })

Some quick points not covered in the comments:

  • config.get() : Returns config values from app/config/default.json
  • DataLoader : Could've been simpler, but I decided to go down that path to showcase one of Koa's best features: support for async functions in middlewares. I'll get to the implementation details in the next section.

3. Reading products data

To demonstrate how Koa plays well with promises, I've built a simple DataLoader component that reads the content of JSON files in a directory and parses them into an array of objects.

The code below makes use of fs-extra to read files content:

const path = require('path')
const fs = require('fs-extra')

function fileInfo(fileName, dir) {
    return {
        slug: fileName.substr(0, fileName.indexOf('.json')),
        name: fileName,
        path: path.join(dir, fileName)
    }
}

function readFile(fileInfo) {
    return fs
        .readJson(fileInfo.path)
        .then(content => Object.assign(content, { _slug: fileInfo.slug }))
}

class DataLoader {
    constructor(dir) {
        this.dir = dir;
    }

    async all() {
        const fileInfos = (await fs.readdir(this.dir)).map(fileName => fileInfo(fileName, this.dir))
        return Promise.all(fileInfos.map(readFile))
    }

    async single(slug) {
        const fileInfos = (await fs.readdir(this.dir)).map(fileName => fileInfo(fileName, this.dir))
        var found = fileInfos.find(file => file.slug === slug)
        return found ? readFile(found) : null
    }
}

module.exports = DataLoader

Note that in beefier scenarios, this could have been database or remote API calls.

This data loader class will then be used in our routes to fetch products.

4. Showing Koa.js home route

Let's take a look at the home route now:

// app/routes/home.js

module.exports = (router, productsLoader) => {
  router.get('/', async ctx => {
    const products = await productsLoader.all()
    ctx.state.model = {
      title: 'Hey there,',
      products: products
    }
    await ctx.render('home');
  })
}

Simple, isn't it? Loading all products, and passing them down to the view via Koa's context object.

Now, I will focus on the middleware function signature. See that async keyword? It's precisely where Koa.js shines. Its support for promises allows you to write middlewares as async functions, thus getting rid of callback hell. This makes for much cleaner and readable code.

Now, here's what to put in the home.pug template to render your products:

// app/views/home.pug

each product in model.products
  h3=product.name
  p=product.description
  p
    span $#{product.price}
  a(href=`/buy/${product._slug}`) More details

Notice how I am accessing the products array via model.products? That's because by default, koa-views pass the entire ctx.state object to your views. Nifty!

Each product has a "Learn More" link that takes us to /buy/some-product.

6. Enabling e-commerce on your Node.js app

What about selling these products? Before attacking the buy route, quickly add Snipcart to your layout, and you'll be good to go:

// app/views/_layout.pug

script(src='https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js')
script(
  id="snipcart"
  src='https://cdn.snipcart.com/scripts/2.0/snipcart.js'
  data-api-key=settings.snipcartApiKey
)
link(
  rel="stylesheet"
  href="https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css"
)

6.1 The "buy" route

The code looks pretty similar to the home route, except I'm loading a single product:

// app/routes/buy.js

module.exports = (router, productsLoader) => {
  router.get("/buy/:slug", async ctx => {
    const product = await productsLoader.single(ctx.params.slug)
    if (product) {
      ctx.state.model = {
        title: product.name,
        product: product
      }
      await ctx.render('product')
    }
  })
}

In product.pug, add this button to hook your product definition to Snipcart:

// app/views/product.pug

button.snipcart-add-item(
  data-item-id=model.product.id
  data-item-name=model.product.name
  data-item-url=urlWithoutQuery
  data-item-price=model.product.price
  data-item-description=model.product.description
  data-item-image=model.product.image
) Add to cart

Well done, you can now sell your products!

node-js-products

6.2 The "donate" route

When confirming an order, Snipcart crawls all the products URLs and validates items price to make sure nothing fishy happened to your cart. To do that, Snipcart looks at the data-item-price attribute of your buy buttons.

Now, since donation amounts are customer-driven, you'll have to use a little trick to make it all work. You have to add the number as a query parameter in the data-item-url attribute of the buy button. Then, make sure that the value is rendered in the data-item-price attribute.

Your app must handle the amount parameter correctly, which brings us to the donate route code:

// app/routes/donate.js

const config = require('config')

module.exports = router => {
  router.get("/donate", async ctx => {
    ctx.state.model = {
      title: "Donate",
      amount: ctx.query.amount || config.get("settings.defaultDonation")
    }
    await ctx.render('donate')
  })
}

Just add an amount property to the model object and assign the query parameter to it.

Here I also used the settings.defaultDonation config value as a fallback when no query parameter is set.

Now, what about donate.pug? Define your elements as follows:

// app/view/donate.pug

label(for="amount") Please enter your donation amount below
input#amount.(type="number", value=model.amount)
button#donate.snipcart-add-item(
data-item-id="donation"
data-base-url=urlWithoutQuery
data-item-url=`${urlWithoutQuery}?amount=${model.amount}`
data-item-name="Donation"
data-item-description="Can't thank you enough!"
data-item-price=model.amount
data-item-shippable="false"
data-item-categories="donations"
data-item-max-quantity="1"
data-item-taxable=false
) Add to cart

Two things to note here:

  • data-item-url is fully generated using urlWithoutQuery and model.amount
  • data-base-url will be used in the script below to recompute data-item-url dynamically at runtime

Finally, write some frontend code to hook up the donation amount input to your buy button:

// app/static/scripts/donate.js

$(function () {
  document
    .querySelector('#amount')
    .addEventListener('change', function (evt) {
      const amount = evt.target.value
      let data = $('#donate').data() // Snipcart relies on jQuery data object
      data.itemPrice = amount
      data.itemId = `donation`
      data.itemUrl = `${data.baseUrl}?amount=${amount}`
    })
});

With that in place, any change made to the #amount field value will update the product URL.

And you're all set!

Live demo & GitHub repo

node-js-ecommerce-demo

See the live demo here

See GitHub repo here

Closing thoughts

I enjoyed Koa very much. It's API is elegant and easy to learn. The architecture puts the developer 100% in control of what's happening, which is nice when you want to build things the way you like. I definitely recommend this approach for any Node.js developer dealing with e-commerce.

I spent less than a day to build this demo including research around Koa.js and post-review tweaks.

To push it further, I could've made use of some cool community middlewares to make it more like a real production app (i.e., request caching, logging). Koa.js is a killer tool to build lean, performant and maintainable web APIs.


If you've enjoyed this post, please take a second to share it on Twitter. Got comments, questions? Hit the section below!

Suggested posts: