JAMstack has been a buzz word for a while now. In online forums and chat channels, you’ll frequently see similar questions on the subject:
What’s the idea behind it? What makes up the JAM? Why is it useful? Is it specific to certain frameworks? How can I build a JAMstack website?
We’ll answer these questions in this tutorial. We’ll also build an ecommerce website by applying the techniques learned with the Next.js framework.
N.B., to follow this tutorial, you’ll need a basic understanding of React and Node.js. Please ensure that you have Node and npm/yarn installed before you begin.
The JAMstack is a modern architectural pattern for building web applications. The JAM stands for Javascript, APIs, and Markup. It’s based on the idea of separating your frontend presentation layer from any dynamic functionality.
With JAMstack, you ship all your webpage presentation markup so the user is presented with information as soon as the site is loaded.
Then you use Javascript to dynamically add functionalities to your application — usually through APIs.
Using the JAMstack saves developers time and effort because it takes away the need to set up servers or backend.
With serverless platforms and APIs, such as Firebase, 8base, and Amazon Lambda, which enable connections from the front-end frameworks, developers can now leverage these platforms alongside other reusable APIs for adding back-end, data storage and processing, authentication, and other dynamic capabilities into their applications.
There are a lot of benefits that come with using JAMstack. When implemented, it can save you time and overhead costs.
With JAMstack, you get:
Typically, with JAMstack, pre-built markup and assets are served over a CDN. This means that as soon as your code is deployed, the CDN gets updated. This guarantees a faster loading speed because nothing beats pre-built files served over a CDN.
Technically — since there’s no database— it can’t be hacked. JAMstack takes away the need to worry about server or database vulnerabilities. You can also leverage the domain expertise of specialist third-party services.
The hosting of static files is cheap or even free. Since your files can be served anywhere via a CDN, scaling is a matter of serving those files in more places. CDN providers will scale up to account for the amount of traffic it receives.
Developers can focus on working on parts of the application that suits their skillset without having to deal with setting up anything else. It allows for more targeted development and debugging, and the expanding selection of CMS options for site generators removes the need to maintain a separate stack for content and marketing.
Over time, numerous open-source static website generators have become available: GatsbyJS, Hugo, Nuxt.js, Next.js, Jekyll, Hexo, VuePress, etc — all of which can be used for generating prebuilt markup, which can serve your website as static HTML files.
Most of the time, the content is managed through static (ideally Markdown) files or a content API.
We’d use Next.js to build a JAMstack website to illustrate some of the points above.
Next.js is a React framework built by Zeit, and according to nextjs.org:
With Next.js, you can build server-side rendering and static web applications using React. There is absolutely no need for any configuration with webpack or anything similar. Just install it and start building.
Here are some other cool features Next.js brings to the table:
To start, create a sample project by running the following commands:
mkdir nextjs-shopping-cart cd nextjs-shopping-cart npm init -y npm install --save react react-dom next
We need to add commands to start up Next.js. Open your package.json
and update the scripts object with the following code:
// ./package.json "scripts": { "dev" : "next", "build": "next build", "start": "next start" }
We’d start by creating the base components necessary to give our website a good look.
In the root of your application, create a components folder with the following files:
// components/Navbar.js const Navbar = (props) => { return ( <nav className="navbar navbar-light bg-light"> <h3>Shoppr</h3> <a className="btn btn-outline-success my-2 my-sm-0">Cart</a> </nav> ); };
// components/Footer.js const Footer = () => { const mystyle = { "position": "absolute", "bottom": "0", "width": "100%", "backgroundColor": "#333", "color":"#fff", }; return ( <footer style={mystyle} className="page-footer font-small bg-blue pt-4"> <div className="container text-center text-md-left"> <div className="row"> <div className="col-md-6 mt-md-0 mt-3"> <h5 className="text-uppercase font-weight-bold">Contact Us</h5> <p>You can contact us on 234-8094-34033-33</p> </div> <div className="col-md-6 mb-md-0 mb-3"> <h5 className="text-uppercase font-weight-bold">Return Policy</h5> <p>We accept returns after 7 days max</p> </div> </div> </div> <div className="footer-copyright text-center py-3">© 2019 Copyright: <span> Shoppr</span> </div> </footer> ); }; export default Footer;
Now, we need to create a shared layout for our application. Our application will need a head section that will contain CSS links, meta-tags, and other related info.
Create a Layout.js
file inside the components folder and add the following code to it:
// components/Layout.js import Head from 'next/head' import Navbar from './Navbar.js' import Footer from './Footer.js' function Layout(props) { return ( <div> <Head> <title>Shopping Cart</title> <meta name="viewport" content="initial-scale=1.0, width=device-width" /> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"/> </Head> <Navbar/> <div className="container-fluid">{props.children}</div> <Footer/> </div> ) } export default Layout
Here, we’ve created a universal layout component that would add an HTML head section to all its children components. We also added the Navbar and Footer since both components would be the same across all pages.
So far, we’ve created the basic components and universal layout for our app. We need to see this in action by creating a page that utilizes them. The page would also be responsible for fetching and displaying our products.
To fetch data from an API we would make use of isomorphic-unfetch
library. Run the following command in your terminal to install it:
npm install --save isomorphic-unfetch
After installation, create a pages
directory and an index.js
file inside it, then add the following code:
// pages/index.js import Products from '../components/Products.js' import fetch from 'isomorphic-unfetch' const Index = (props) => ( <Products products={props.products}/> ) Index.getInitialProps = async function() { const res = await fetch('https://my-json-server.typicode.com/mood-board/demo/products'); const data = await res.json(); return { products: data }; }; export default Index
To fetch data from a remote source for a component, you’ll use the getInitialProps
function that comes out of the box with Next.js.
In our code, we receive our product list from an API and set it to the product props. We then populate our product listing by passing it down to the products
component as props.
Before we try it out on the browser, create two new components: products.js
, product.js
and add the following code to it:
// components/products.js import React, { Component } from 'react'; import Product from './Product'; class Products extends Component { constructor(props) { super(props); } render() { return ( <div> <div className="container mt-4"> <div className="row"> {this.props.products.map(item => <Product key={item.id} product={item}/>)} </div> </div> </div> ) } }; export default Products;
Here, we pass down the item to be displayed by the product component. Create a Product.js
file and add the following code to it:
// components/product.js import Link from "next/link"; const Product = (props) => { return ( <div className="col-sm-4"> <div className="card" style={{width: "18rem"}}> <img src={props.product.image} className="card-img-top" alt="shirt"/> <div className="card-body"> <h5 className="card-title">{props.product.name}</h5> <h6 className="card-title">$ {props.product.price}</h6> <Link href="/products/[id]" as={`/products/${props.product.id}`}> <a>View Item →</a> </Link> </div> </div> </div> ); } export default Product;
Now visit https://localhost:3000 in your browser and you will see the following:
Our homepage displays all our products. We need to create individual pages for each of the products so we can get more information about them before adding them to our cart.
From our API structure, we can retrieve a single product with the URL /api/products/{id}
. However, we do not know the ids of our products beforehand so we cannot manually create pages for them. We need a way to dynamically generate the pages on the fly.
Next.js allows us to do this in a unique way. In your pages folder, create a sub product folder and a file name [id].js
then add the following code to it:
// pages/products/[id].js import fetch from 'isomorphic-unfetch'; import ProductItem from '../../components/ProductItem' const Productitem = props => { return ( <ProductItem product={props.product}/> ) }; Productitem.getInitialProps = async function(context) { const { id } = context.query; const res = await fetch(`https://my-json-server.typicode.com/mood-board/demo/products/${id}`); const product = await res.json(); return { product }; }; export default Productitem;
Next.js allows us to automatically grab URL values through a special object called context.
Once we grab the id from the URL, we query the API to get the product and pass it to our component as initial props.
Before we check it on the browser, create a ProductItem.js
in the components folder and add the following code:
// components/productitem.js const ProductItem = (props) => { return ( <div className="container mt-4"> <div className="row"> <div className="col-sm-4"> <div className="card" style={{width: "18rem"}}> <img src={props.product.image} className="card-img-top" alt="shirt"/> </div> </div> <div className="col-sm-4 "> <div className="card-body" style={{marginTop: "135px"}}> <h5 className="card-title">{props.product.name}</h5> <h6 className="card-title">$ {props.product.price}</h6> <p>{props.product.description}</p> <button className="btn btn-large btn-primary">Add To Cart</button> </div> </div> </div> </div> ); } export default ProductItem;
Now, when you click on the link from the homepage you’ll see this:
To wrap up, we need to add the cart functionality to our application using React Context API and the browser’s local storage.
Context makes it possible to pass data through the component tree without having to pass props down manually at every level.
To be able to share state globally via context, the whole application needs to be wrapped in the context provider. Next.js allows us to override the default wrapper for an application using a special file called _app.js
.
First, let’s create our context file. Inside the components directory create a file called cartContext.js
and add the following code to it:
// components/cartcontext.js import { createContext } from 'react'; const CartContext = createContext(); export default CartContext;
Here, we import the createContext function from React. To create a new context all we need to do is call createContext() and attach it to a variable. A context can also be initialized with a default value but we won’t need that in our case.
Next, in the pages directory create a file called _app.js
and add the following code:
// pages/_app.js import App from 'next/app' import Layout from '../components/layout.js' import cartcontext from '../components/cartContext'; export default class MyApp extends App { state = { cart : [], carttotal : 0 } componentDidMount = () => { const cart = JSON.parse(localStorage.getItem('cart')); const carttotal = JSON.parse(localStorage.getItem('total')); if (cart) { this.setState({ cart, carttotal }); } }; addToCart = (product) => { this.setState({ cart: [...this.state.cart, product] }); localStorage.setItem('cart', JSON.stringify(this.state.cart)); } calculateTotal = (price) => { this.setState({ carttotal: this.state.carttotal + price }); localStorage.setItem('total', JSON.stringify(this.state.carttotal)); } render () { const { Component, pageProps } = this.props return ( <cartcontext.Provider value={{cart: this.state.cart, addToCart: this.addToCart, total: this.calculateTotal, carttotal: this.state.carttotal}}> <Layout> <Component {...pageProps} /> </Layout> </cartcontext.Provider> ) } }
Here, we’ve wrapped our entire application with our newly created context provider. This gives all our components access to the values stored in the context.
Along with the context provider, we sent two values (cart
, carttotal
) to hold the cart items and the total cost.
We’ve also passed down two methods (addToCart
, total
) to enable adding to cart and calculating the total price.
When our component mounts (via componentDidMount()
), we retrieve the values stored in our local storage and set the state of our cart and total price.
We also update the records stored in our local storage whenever the addToCart
and calculateTotal
functions are triggered.
To use the data in different parts of the application, we can import the CartContext
component inside any other component that needs it using a single line of code:
const { cart } = useContext(CartContext);
We need to access our components in two places: our navbar
to update the cart item count, and our product-item
to add the item to the cart.
Open the Navbar.js
and update it with the following code:
// components/Navbar.js import React from 'react'; import { useContext } from 'react'; import CartContext from './cartContext'; const Navbar = (props) => { const { cart } = useContext(CartContext); return ( <nav className="navbar navbar-light bg-light"> <h3><a href="/">Shoppr</a></h3> <a href="/cart" className="btn btn-outline-primary my-2 my-sm-0">Cart {cart.length}</a> </nav> ); }; export default Navbar;
Through the useContext
Hook provided by React, we retrieved the cart items and can display the count whenever it increases. That way the shopper can see the number of items in the cart at any time.
Next, open the ProductItem.js
and update it with the following code:
// components/ProductItem.js import Link from "next/link"; import { useContext } from 'react'; import CartContext from './cartContext'; const ProductItem = (props) => { const { addToCart, total } = useContext(CartContext); return ( <div className="container mt-4"> <div className="row"> <div className="col-sm-4"> <div className="card" style={{width: "18rem"}}> <img src={props.product.image} className="card-img-top" alt="shirt"/> </div> </div> <div className="col-sm-4 "> <div className="card-body" style={{marginTop: "135px"}}> <h5 className="card-title">{props.product.name}</h5> <h6 className="card-title">$ {props.product.price}</h6> <p>{props.product.description}</p> <button className="btn btn-large btn-primary" onClick={() => {addToCart(props.product); total(props.product.price); }}>Add to Cart </button> </div> </div> </div> </div> ); } export default ProductItem;
Here, we’ve retrieved the functions needed for adding to the cart and calculating our prices. We trigger them when the user clicks the Add To Cart button.
Finally, we need a page to display our cart items. Inside the pages directory, create a file called cart.js
and add the following code to it:
// pages/cart.js import { useContext } from 'react'; import CartContext from '../components/cartContext'; const Cart = () => { const { cart, carttotal } = useContext(CartContext); return( <div> <h3 >Cart Items</h3> <div className="pb-5"> <div className="container"> <div className="row"> <div className="col-lg-12 p-5 bg-white rounded shadow-sm mb-5"> <div className="table-responsive"> <table className="table"> <thead> <tr> <th scope="col" className="border-0 bg-light"> <div className="p-2 px-3 text-uppercase">Product</div> </th> <th scope="col" className="border-0 bg-light"> <div className="py-2 text-uppercase">Price</div> </th> </tr> </thead> <tbody> {cart.map(item => <tr> <th scope="row" className="border-0"> <div className="p-2"> <img src={item.image} alt="product" width="70" className="img-fluid rounded shadow-sm"/> <div className="ml-3 d-inline-block align-middle"> <h5 className="mb-0"> <a href="#" className="text-dark d-inline-block align-middle">{item.description}</a></h5> </div> </div> </th> <td className="border-0 align-middle"><strong>$ {item.price}</strong></td> </tr> )} </tbody> </table> </div> <ul className="list-unstyled mb-4"> <li className="d-flex justify-content-between py-3 border-bottom"><strong className="text-muted">Total</strong> <h5 className="font-weight-bold">$ {carttotal}</h5> </li> </ul> <a href="#" className="btn btn-dark rounded-pill py-2 btn-block">Procceed to checkout</a> </div> </div> </div> </div> </div> ) } export default Cart;
Here, we retrieve and display the items in our cart through the cart context.
In this section, we will deploy our app to Netlify.
If you don’t already have an account with Netlify, you can create a new site from here.
From the dashboard, click on New site from Git:
Next, add your GitHub account by clicking on the link:
In the next step, we need to add the command that would start our application:
Once the site is deployed, we can view it live at https://focused-agnesi-b7664a.netlify.com/
. In your case, the URL will be different. We can view the URL of our site from our project’s overview section:
Now, whenever you push new code to your GitHub repository, Netlify will auto-deploy your site and update it’s CDN leading to a very fast load time for your website.
In this tutorial, we talked about the JAMstack and built a sample application with it. You should note that the JAMstack is still a growing concept and only works best in certain cases.
In many cases, having a full-stack application with a properly created backend is necessary. To read more about JAMstack, check out the docs here.
The sample application can be found here.
LogRocket is like a DVR for web and mobile apps and websites, recording literally everything that happens on your ecommerce app. Instead of guessing why users don’t convert, LogRocket proactively surfaces the root cause of issues that are preventing conversion in your funnel, such as JavaScript errors or dead clicks. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Start proactively monitoring your ecommerce apps — try LogRocket for free.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
One Reply to "Building a JAMstack ecommerce website"
How to add CMS to it. For adding more products and managing orders?
Please do share it.