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:
- Initializing the Koa.js app directory.
- Creating the app’s entry point.
- Reading products data.
- Setting up Koa.js routes.
- 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 fromapp/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 usingurlWithoutQuery
andmodel.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!
Leave a Reply