A Guide to Node.js E-Commerce (w/ Koa.js Tutorial)

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

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

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

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

Why use Node.js for e-commerce?

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.

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.

Node.js never gets bigger that you need it to be. Performance-wise that’s gold.

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.

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!

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

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).
  • pug : The templating engine I’ll use in this demo.
  • 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 Node.js 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!

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!

6.2 The “donate” route

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
  • 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.

Live demo & GitHub repo

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!

Be the first to comment

Leave a Reply

Your email address will not be published.


*