Author: Richard Haines

Posted: 25 Mar 2020

take me there

Im my previous post First look at RedwoodJS i took a look at Redwood with fresh eyes and documented what i found interesting. I did have an idea to just outline adding snipcart to a RedwoodJS project but as i went through the process and took notes i came to the conclusion that maybe a tutorial would be a better way to go.

So this is what you might call a simple tutorial, by that i mean that we are not going to make a full blown ecommerce website, rather we are going to setup a RedwoodJS site and add snipcart to it. By the end of this tutorial we will have a website up and running and be able to sell products. Lets GOOOOOOOO 🕺

This tutorial assumes that you have never used RedwoodJS, that you havent even read my previous first look post!! OMG!

The end result will look like this:, except we are going to go one better. We are going to add an admin route with CRUD operations that is accessed via sign up and login using Netlify Identity. 😱

From the command line lets create our RedwoodJS project:

1yarn create redwood-app <whatever-you-want-to-call-it>

Create a new repo in github and give it the same name you used when creating your RedwoodJS app. Now navigate into the projects root and create a git repo.

1git init
2git add .
3git commit -m "My first commit"
4git remote add origin <your-github-repo-url>
5git push -u origin master

Base layout and project files

We are going to use Theme-ui to style our website because its super simple and powerful. Lets install it, remembering that we are working in yarn workspaces so we need to prefix our install with workspaces and the workspace we want to install the package in.

1yarn workspace web add theme-ui

Now that we have theme-ui installed we need to add it to our project. In the index.js file located at the web projects root add the ThemeProvider component.

1import ReactDOM from "react-dom";
2import { RedwoodProvider, FatalErrorBoundary } from "@redwoodjs/web";
3import FatalErrorPage from "src/pages/FatalErrorPage";
4import { ThemeProvider } from "theme-ui";
5import theme from "./theme";
7import Routes from "src/Routes";
9import "./scaffold.css";
10import "./index.css";
13 <ThemeProvider theme={theme}>
14 <FatalErrorBoundary page={FatalErrorPage}>
15 <RedwoodProvider>
16 <Routes />
17 </RedwoodProvider>
18 </FatalErrorBoundary>
19 </ThemeProvider>,
20 document.getElementById("redwood-app")

We are wrapping the ThemeProvider around our whole app so that everything gets our styles. But where are those styles coming from i hear you ask? That would be the theme.js file. Lets create that now inside our src directory.

1export default {
2 useCustomProperties: false,
3 fonts: {
4 body: "Open Sans",
5 heading: "Montserrat"
6 },
7 fontWeights: {
8 body: 300,
9 heading: 400,
10 bold: 700
11 },
12 lineHeights: {
13 body: "110%",
14 heading: 1.125,
15 tagline: "100px"
16 },
17 letterSpacing: {
18 body: "2px",
19 text: "5px"
20 },
21 colors: {
22 text: "#FFFfff",
23 background: "#1a202c",
24 primary: "#000010",
25 secondary: "#E7E7E9",
26 secondaryDarker: "#2d3748",
27 accent: "#DE3C4B"
28 },
29 breakpoints: ["40em", "56em", "64em"]

Its all pretty self explanatory but if you need a reresh or have no idea what the hell this is then you can check our the Theme-ui docs.

Ok nice. You don't need to run the project yet, lets do it blind and be surprised by the results!! Our standard RedwoodJS project gives us folders but not much else in the way of pages or components. Lets add our home page via the RedwoodJS CLI.

1yarn rw g page home /

So what is going on here i hear you scream at the screen?? Well we are basically saying redwood (rw) can you generate (g) a page called home at route (/) which as we all know, because we are all professionals here, the root route.

RedwoodJS will now generate two new files, one called HomePage (RedwoodJS prefixes the name we give in the command with page, because its nice like that) and a test file. Which passes! Of course this is just a render test and if we add more logic we should add tests for it in this file.

We can leave the home page for a second and run some more RedwoodJS CLI commands because they are amazing and give us lots of stuff for free! All together now....

1yarn rw g page contact
2yarn rw g layout main

We wont go through actually adding the contact form page in this tutorial but you can check the RedwoodJS docs to get a good idea of how to do it and why they are pretty sweet.

We have created a contact page and a layout which we have called main. Our MainLayout component which was created in a new folder called MainLayout will hold the layout to our website. This is a common pattern used in Gatsby where you create a layout component and import and wrap all other components that are children to it. Lets take a look at out MainLayout component.

1import { Container } from "theme-ui";
3const MainLayout = ({ children }) => {
4 return (
5 <Container
6 sx={{
7 maxWidth: 1024
8 }}
9 >
10 <main>{children}</main>
11 </Container>
12 );
15export default MainLayout;

Pretty simple right? But we want to have a header on all our pages which displays our website name and any links we may have to other pages in our site. Lets make that now.

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import { Link, routes } from "@redwoodjs/router";
5const Header = () => {
6 return (
7 <header
8 sx={{
9 display: "flex",
10 justifyContent: "space-between",
11 alignItems: "center",
12 borderBottom: "solid 2px",
13 borderColor: "secondaryDarker"
14 }}
15 >
16 <h1>
17 <Link
18 sx={{
19 fontFamily: "heading",
20 fontWeight: 400,
21 color: "text",
22 textDecoration: "none",
23 ":hover": {
24 color: "accent"
25 }
26 }}
27 to={routes.home()}
28 >
29 Redwood - Snipcart
30 </Link>
31 </h1>
32 <nav
33 sx={{
34 display: "flex",
35 justifyContent: "space-evenly",
36 width: "15em"
37 }}
38 >
39 <Link
40 sx={{
41 fontFamily: "heading",
42 fontWeight: 400,
43 color: "text",
44 ":hover": {
45 color: "accent"
46 }
47 }}
48 to={}
49 >
50 Contact
51 </Link>
52 </nav>
53 </header>
54 );
57export default Header;

Lets add our Header component to the MainLayout to complete it.

1import { Container } from "theme-ui";
2import Header from "src/components/Header";
4const MainLayout = ({ children }) => {
5 return (
6 <Container
7 sx={{
8 maxWidth: 1024
9 }}
10 >
11 <Header />
12 <main>{children}</main>
13 </Container>
14 );
17export default MainLayout;

We still don't know what this looks like! (unless you cheated and looked at the example site!) Lets carry on regardless. We'll use our new layout component to wrap the content of our home page, thus providing us with a consistent look to our site whatever page our visitors are on. Of course we can have different layouts for different pages and if we wanted to do that we could either create them ourselves or use the RedwoodJS CLI to create them for us.

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import MainLayout from "src/layouts/MainLayout/MainLayout";
5const HomePage = () => {
6 return (
7 <MainLayout>
8 <h2
9 sx={{
10 fontFamily: "body",
11 fontWeight: 400
12 }}
13 >
14 Super Duper Ecommerce Website
15 </h2>
16 <p
17 sx={{
18 fontFamily: "body",
19 fontWeight: 400
20 }}
21 >
22 Some text here explaining how great your website is!
23 </p>
24 </MainLayout>
25 );
28export default HomePage;

Note that we don't specify the route like we did when creating the home page (/), this is because RedwoodJS is clever enough to know that we want a new page at he route for the given name. By specifying / in our home page creating we are telling RedwoodJS that this will be our main page/route. Note that when creating pages via the CLI we can use more than one word for our pages but they have to conform to a standard that tells the CLI that is is in fact two word that will be joined together. Any of the following will work.

Taken from the RedwoodJS docs:

1yarn rw g cell blog_posts
2yarn rw g cell blog-posts
3yarn rw g cell blogPosts
4yarn rw g cell BlogPosts

Adding some purchase power

Before we dive into the graphql schema we will add our snipcart script. You will need to create an account with snipcart, once done open the dashboard and click the little person icon in the top right hand corner. You'll want to go to domains & urls first and add localhost:8910 to the domain filed and hit save. This will tell snipcart to look for this domain in dev. Keep the protocal as http as thats what RedwoodJS uses for local dev. Next scroll down to api keys and copy the first line of the code they say to copy. For example:

2 rel="stylesheet"
3 href=""

Open the index.html file at the web projects root and past the style sheet into the head element. next copy the div ans script tags and paste them inside the body tag but below the div with id of redwood-app. It should look like this except your api key will be different.

You can use this api key and keep it in your html file which will be committed to git because, and i quote "The public API key is the key you need to add on your website when including the snipcart.js file. This key can be shared without security issues because it only allows a specific subset of API operations."
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <link rel="icon" type="image/png" href="/favicon.png" />
7 <link
8 href=";700&family=Open+Sans&display=swap"
9 rel="stylesheet"
10 />
11 <link
12 rel="stylesheet"
13 href=""
14 />
15 <title><%= htmlWebpackPlugin.options.title %></title>
16 </head>
17 <body>
18 <div id="redwood-app"></div>
19 <div id="snipcart" data-api-key="<your-api-key-here>" hidden></div>
20 <script src=""></script>
21 </body>

Now that we have added snipcart to our site we can start up our site and see whats what.

1yarn rw dev

Open your dev tools and check the elements tab, check the head and body tags for the snipcart tags/scripts. Don't worry if you don't see your api key in the div at the bottom of the body tag, you're not supposed too. Snipcart will handle that for us. Check your console for any errors and sit back because there aren't any. (i hope 😶)

Adding product models the the graphql schema

Close the web directory and open the api directory. Remove the commented code and add the following product model.

1model Product {
2 id Int @id @default(autoincrement())
3 title String
4 description String
5 price String
6 image String
7 imageAlt String

Next we want to take a snapshot as migration and then apply it. This reminds me of when i used to work with Entity Framework back in my C# days, oh the memories.... 🥴

1yarn rw db save // create the local database
2yarn rw db up // apply the migration and create the table

React, React, React!

Lets code some components. We'll use the RedwoodJS CLI to scaffold out some CRUD components for us.

1yarn rw g scaffold product

This is some kinda magic. We now have numerous files in our components folder.

  • EditProductCell
  • NewProduct
  • ProductForm
  • Products
  • ProductsCell

These files each provide us with admin functionality to manipulate our sites data.

Go through each file and look at the queries at the top of the file. For some reason they will say posts instead of product(s), change them otherwise nothing will work. Also change the query names.

We are going to leave the styling as it is as thats not the focus of this tutorial, but its would be very easy to just remove all the class names and replace them with an sx prop with our theme styles.

Open Product.js and change the image table tr - td to return an img tag.

1<tr className="odd:bg-gray-100 even:bg-white border-t">
2 <td className="font-semibold p-3 text-right md:w-1/5">Image</td>
3 <td className="p-3">
4 <img src={product.Image} alt={product.imageAlt} />
5 </td>

Do the same in the Products.js file except add a width of 150px to the img element tag otherwise the image will be huge in the table that displays them.

1<td className="p-3">
2 <img src={truncate(product.image)} width="150px" alt={imageAlt} />

For this tutorial we will be using some random images form unsplash. We will use a special url with a collection id to get random images for each of our products. Open a new tab and navigate to an example url that we will use looks like this:, pick a fitting alt tag.

Lets create a new cell to handle showing all our products. A cell in RedwoodJS is basically a file that contains.

  • A query to fetch the data we want to showing
  • A loading function to show when the data is loading
  • An empty function to show if there is no data to show
  • A failure function to show when the request has failed to fetch any data
  • A success function which will show the data

Go ahead and add some products by navigating to http//:localhost:8910/products

We can forget about styling the first three and concentrate on then success function. Lets create this cell.

1yarn rw g cell allProducts
We will need to change the query name to products to match our schema. Also change it as the prop in the success function.

Now in our components folder create a new component called ProductsContainer.

1/** @jsx jsx */
2import { jsx } from "theme-ui";
4const ProductsContainer = ({ children }) => (
5 <div
6 sx={{
7 margin: "2em auto",
8 display: "grid",
9 gridAutoRows: "auto",
10 gridTemplateColumns: "repeat(auto-fill, minmax(auto, 450px))",
11 gap: "1.5em",
12 justifyContent: "space-evenly",
13 width: "100%"
14 }}
15 >
16 {children}
17 </div>
20export default ProductsContainer;

Next create a SingleProduct component.

1/** @jsx jsx */
2import { jsx } from "theme-ui";
4const SingleProduct = ({ id, title, description, price, image, imageAlt }) => {
5 return (
6 <div
7 sx={{
8 display: "flex",
9 flexDirection: "column",
10 border: "solid 2px",
11 borderColor: "secondaryDarker",
12 width: "100%",
13 height: "auto",
14 padding: "1.5em"
15 }}
16 >
17 <p
18 sx={{
19 fontFamily: "heading",
20 fontSize: "2em",
21 textAlign: "center"
22 }}
23 >
24 {title}
25 </p>
26 <div
27 sx={{
28 width: "100%",
29 height: "auto"
30 }}
31 >
32 <img src={image} width="400px" alt={imageAlt} />
33 </div>
34 <p
35 sx={{
36 fontFamily: "heading",
37 fontSize: "1em"
38 }}
39 >
40 {description}
41 </p>
42 </div>
43 );
46export default SingleProduct;

Now we can add them to our success function in AllProductsCell.js and pass in the product data.

1export const Success = ({ products }) => {
2 console.log({ products });
3 return (
4 <ProductsContainer>
5 { => (
6 <SingleProduct
7 key={}
8 id={}
9 title={product.title}
10 description={product.description}
11 price={product.price}
12 image={product.image}
13 imageAlt={product.imageAlt}
14 />
15 ))}
16 </ProductsContainer>
17 );

How do we buy stuff?

So we have our products on our site but we cant yet buy them. Lets use snipcart to add a buy button. Its really easy, i promise! Create a snipcart folder inside the components folder and add a file called BuyButton.js. Lets add the content then go through it.

1/** @jsx jsx */
2import { jsx } from "theme-ui";
4const BuyButton = ({ id, title, price, image, description, url, path }) => (
5 <button
6 sx={{
7 fontFamily: "heading",
8 fontWeight: "bold",
9 border: "1px solid",
10 borderRadius: "5px",
11 padding: "0.35em 1.2em",
12 borderColor: "secondaryDarker",
13 backgroundColor: "secondary",
14 color: "background",
15 cursor: "pointer",
16 textTransform: "uppercase",
17 height: "2.5em",
18 "&:hover": {
19 color: "accent",
20 backgroundColor: "background",
21 fontWeight: "bold"
22 },
23 "&:active": {
24 boxShadow: "-1px 1px #00001F"
25 }
26 }}
27 className="snipcart-add-item"
28 data-item-id={id}
29 data-item-price={price}
30 data-item-image={image}
31 data-item-name={title}
32 data-item-description={description}
33 data-item-url={url + path}
34 data-item-stackable={true}
35 data-item-has-taxes-included={true}
36 >
37 Buy Now
38 </button>
41export default BuyButton;

Snipcart works by recognizing the className we add to the element, as well as the path of the product. It also expects certain properties on that element. These are the base properties expected, you can also add variants but we wont cover that here. You can check out the docs for more info.

We can now add the BuyButton to our SingleProduct component.

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import BuyButton from "./snipcart/BuyButton";
5const SingleProduct = ({ id, title, description, price, image, imageAlt }) => {
6 return (
7 <div
8 sx={{
9 display: "flex",
10 flexDirection: "column",
11 border: "solid 2px",
12 borderColor: "secondaryDarker",
13 width: "100%",
14 height: "auto",
15 padding: "1.5em"
16 }}
17 >
18 <p
19 sx={{
20 fontFamily: "heading",
21 fontSize: "2em",
22 textAlign: "center"
23 }}
24 >
25 {title}
26 </p>
27 <div
28 sx={{
29 width: "100%",
30 height: "auto"
31 }}
32 >
33 <img src={image} width="400px" alt={imageAlt} />
34 </div>
35 <p
36 sx={{
37 fontFamily: "heading",
38 fontSize: "1em"
39 }}
40 >
41 {description}
42 </p>
43 <BuyButton
44 id={id}
45 title={title}
46 price={price}
47 description={description}
48 image={image}
49 url="https://<your-netily-site-name>"
50 path="/store"
51 />
52 </div>
53 );
56export default SingleProduct;

Now as you can see above i have used the netlify deployed url for the product url. When in dev you van use localhost:8910. The reason i left this in the example is to try and remind you that you will have to change this when deploying otherwise snipcart wont recognize the products url. On that note lets commit and push our changes.

Our site is ready to go live. We have setup a simple ecommerce website with minimal effort. Of course there is much more we can do, and will do! I wont cover deployment in this tutorial, you can check the awesome docs. In the next tutorial we will add Netlify Identity with a protected route so that our admins can add and edit products from within the website. I hope you enjoyed this, let me know what you think on twitter! 😊

Edit on GitHub.Previous: First look at RedwoodJSNext: React context with TypeScript