How to Build an eCommerce Site (from scratch)

Matt Hough
10 min readMar 28, 2018

--

A while ago I recommended people to ditch Shopify and do it yourself”. That guide was just theoretical and went over what tools to use, expecting someone could figure it out themselves. Today, I have decided to create a recount/tutorial of how I did it to help people learn how to build an ecommerce site themselves and ditch Shopify! Any steps throughout this are a guide. They may not be the best steps, but they are what enabled me to build a one-product-store quickly and easily.

If you are interested in the store I made, checkout habitsocks.com.

Stack

For this project I used Ruby on Rails for the backend of the website, React.js for the frontend, and Express.js for the backend Stripe server. I could have used Ruby for the backend Stripe payments, however this was mainly a Javascript project, so I went with the Express server.

The one-product-store I built — habitsocks.com

1. The Homepage/Frontend

Before I actually start wiring things up, like the checkout, I prefer to make everything look right first. If you want a website that looks good on a time budget and you aren’t a web designer, don’t shy away from website templates! I used the Stack theme found on ThemeForest, but you can use any theme you want of course!

2. Doing Cool Things: localStorage for the cart

A dilemma I had when making my simple one-product-store was that users never signed in, but I still wanted to store their cart so that when they closed their browser window and opened it up again, their lovely socks would still be there! To do this I used a cool thing called localStorage in Javascript, where I stored the quantity of the user’s cart. If you are interested, Medium is actually using localStorage themselves. To check it out, go to the developer console and type localStorage. To see how this works with a shopping cart, let’s practice: In the developer console type localStorage.setItem('cart', 1). You just stored a string (the number 1) in the client’s localStorage. To access that string, type localStorage.getItem('cart'). Now if you want you can try getting whatever data Medium has stored on your machine. Note that localStorage has limits on the amount that you can store.

In order to add an item to their cart, a user would click on the ‘add to cart’ react component.

Homepage with add to cart button

This component would work alongside the navbar cart icon which showed users how many items were in their cart.

See mini cart on the right of the navbar

Here are the meat and potatoes of how that worked. First the add to cart component would render a button (obviously) and handleClick would update localStorage with the new cart quantity when the user clicked the button. For simplicity after the user clicked the add to cart button they would be redirected to their cart.

import React from 'react';const AddToCart = () => {
const handleClick = () => {
const cart = parseInt(localStorage.getItem('cart'));
const newCart = cart + 1;
cart ? localStorage.setItem('cart', newCart) : localStorage.setItem('cart', 1);
window.location.href = '/cart';
}
return (
<a className='btn btn--primary type-uppercase' onClick={handleClick}>
<span className='btn__text'>Add to Cart</span>
</a>
);
}
export default AddToCart;

When the navbar cart component mounts, the quantity in state is set to the quantity that is stored in localStorage.

It is worth noting that for some weird reason, storage change events, which are just events that are called when you change localStorage, are only fired in different windows. Listening to a storage change will not work in the same window.

import React from 'react';class MiniCart extends React.Component {
constructor(props) {
super(props);
this.state = {
quantity: localStorage.getItem('cart') || 0
}
}
handleClick() {
window.location.href = '/cart';
}
render () {
return (
<div className='cart--nav' onClick={this.handleClick}>
<i className='fa fa-shopping-bag cart--nav--icon'></i>
<span className='cart--number'>{this.state.quantity}</span>
</div>
);
}
}
export default MiniCart;

3. The Cart View & Checkout

Now that we have the homepage setup, we need to setup the cart view. First, I stubbed out what the cart should look like. Finding some nice-ish cart styling on the world wide web.

Rails was used to store the products themselves. I set up a simple Product model for this and rendered my product as json so it could be accessed by the frontend. When the react component mounted I used Javascript’s built in ‘fetch’ method to get the product I needed. A better way of doing this would have been to get the product by it’s id through an endpoint like /api/product/:id. However, considering I was only selling one product I didn’t think it mattered that much.

Once that was done I was able to setup the quantity changing options. These options were pretty simple as well. I had a number input field with the quantity from localStorage and when that was changed the changeQuantity callback was fired. All the changeQuantity callback did was change the localStorage quantity and call setState to re-render the page. The if statement in the method just regulated what could be entered into the box. In this case, that was any number greater than or equal to 1 and less than or equal to 99.

The remove button worked by simply binding a callback to the onClick event. That would just set the localStorage quantity to 0 and call setState to re-render the page.

There are a few different conditionals in the render method. The first one I used to display a little spinner while waiting for the content from the rails API to load. The second conditional just displayed an empty cart message.

Loading spinner
Empty cart message
import React from 'react';
import Checkout from './Checkout';
export default class ViewCart extends React.Component {
constructor(props) {
super(props);
this.state = {quantity: localStorage.getItem('cart')};
}
componentWillMount() {
fetch('/api/products')
.then(response => {
response.json().then(data => {
const sockInfo = data['data'][0]['attributes'];
this.setState({
sockInfo: {
name: sockInfo.name,
imageURL: sockInfo.image_url,
price: sockInfo.price.toFixed(2),
description: sockInfo.description,
shippingInfo: sockInfo.shipping_info
}
})
});
});
}
removeItem() {
localStorage.removeItem('cart');
this.setState({quantity: 0});
}
changeQuantity(ev) {
const newQuantity = parseInt(ev.target.value);
if (newQuantity < 1 || newQuantity > 99 || !newQuantity) return;
localStorage.setItem('cart', newQuantity);
this.setState({quantity: newQuantity});
}
render() {
const { quantity, sockInfo } = this.state;
if (!sockInfo) return <div className='spinner'></div>;

if (quantity < 1) return (
<div className='row no--items'>
<h2 className='text-center'>There are no items in your cart</h2>
</div>
)
const total = (sockInfo.price * quantity).toFixed(2);
return (
<div className="view--cart">
<div className="column-labels">
<label className="product-image">Image</label>
<label className="product-details">Product</label>
<label className="product-price">Price</label>
<label className="product-quantity">Quantity</label>
<label className="product-removal">Remove</label>
<label className="product-line-price">Total</label>
</div>
<div className="product">
<div className="product-image">
<img src={sockInfo.imageURL} />
</div>
<div className="product-details">
<div className="product-title">{sockInfo.name}</div>
<p className="product-description">{sockInfo.description}</p>
</div>
<div className="product-price">{sockInfo.price}</div>
<div className="product-quantity">
<input type='number' value={quantity} onChange={this.changeQuantity.bind(this)} min="1" />
</div>
<div className="product-removal">
<a className='btn btn--primary type-uppercase' onClick={this.removeItem.bind(this)}>
<span className='btn__text'>Remove</span>
</a>
</div>
<div className="product-line-price">{total}</div>
</div>
<div className="totals">
<div className="totals-item">
<label>Subtotal</label>
<div className="totals-value" id="cart-subtotal">{total}</div>
</div>
<div className="totals-item">
<label>Shipping</label>
<div className="totals-value" id="cart-shipping">0.00</div>
</div>
<div className="totals-item totals-item-total">
<label>Grand Total</label>
<div className="totals-value" id="cart-total">{total}</div>
</div>
<div className="totals-item totals-item-total checkout--container">
<Checkout name='Habit Socks' description="Great education for all children" amount={parseFloat(total)} quantity={quantity} productSku={this.props.productSku} />
</div>
</div>
</div>
);
}
}

The Checkout component rendered by the cart was taken from Robin Wieruch’s awesome blog post. It makes use of the react-stripe-checkout library. I modified both the library and the Checkout component a bit to work for my application. You can see the Checkout component below. If you want to see my version of the react-stripe-checkout library, click here. It allows a bit more customization for styling etc.

Basically this component just renders the StripeCheckout button and deals with submitting it. The onToken method deals with actually sending to the Express.js backend server and once that data comes back the successPayment method deals with a successful payment by sending a Postal.js message, telling its parent the payment was a success. Obviously, if the payment failed then the errorPayment method would be called.

import React from 'react';
import postal from 'postal';
import $ from 'jquery';
import StripeCheckout from './StripeCheckout';
import STRIPE_PUBLISHABLE from '../constants/stripe';
import PAYMENT_SERVER_URL from '../constants/server';
const CURRENCY = 'AUD';const fromAUDToCent = amount => amount * 100;const successPayment = data => {
localStorage.removeItem('cart');
postal.publish({
channel: 'checkout',
topic: 'checkout.success',
data: {
checkout: 'success',
data: JSON.stringify(data)
}
});
};
const errorPayment = data => {
console.log('Payment Error:', data);
};
const onToken = (description, quantity, productSku) => token => {
const shipping = {
name: token.card.name,
address: {
line1: token.card.address_line1,
city: token.card.address_city,
state: token.card.address_state,
country: token.card.address_country,
postal_code: token.card.address_zip
}
}
const items = [{
type: 'sku',
parent: productSku,
quantity: quantity
}];
$.post(PAYMENT_SERVER_URL, {
pay: { source: token.id },
order: {
email: token.email,
shipping: shipping,
items: items,
currency: CURRENCY
}
}, successPayment).fail(errorPayment);
}
class Checkout extends React.Component {
static defaultProps = {
buttonClassName: 'btn btn--primary type-uppercase checkout--button',
buttonTextClassName: 'btn__text',
label: 'Checkout'
}
render() {
const { name, description, amount, quantity, productSku, buttonClassName, buttonTextClassName, label } = this.props;
return (
<StripeCheckout
className={buttonClassName}
buttonTextClassName={buttonTextClassName}
name={name}
description={description}
amount={fromAUDToCent(amount)}
token={onToken(description, quantity, productSku)}
currency={CURRENCY}
stripeKey={STRIPE_PUBLISHABLE}
allowRememberMe={false}
label={label}
shippingAddress={false}
billingAddress={true}
/>
)
}
}
export default Checkout;

The parent of the aforementioned component is the CheckoutFlow component. This either returns the checkout success component or the cart view component depending on whether the payment was a success or not. Or even made at all. It does this by listening to the postal channel ‘checkout’ and calling the checkoutSuccess callback method when it receives a message. checkoutSuccess then sets the state, triggering a re-render of the component. If the payment was a success it would show a page with a thank you message.

import React from 'react';
import postal from 'postal';
import CartView from './CartView';
import CheckoutSuccess from './CheckoutSuccess';
class CheckoutFlow extends React.Component {
constructor(props) {
super(props);
this.state = {
success: false,
checkoutData: []
}
}
componentDidMount() {
postal.subscribe({
channel: 'checkout',
topic: 'checkout.success',
callback: this.checkoutSuccess.bind(this)
});
}
checkoutSuccess(data, env) {
const checkoutData = JSON.parse(data.data).success;
if (checkoutData.status === 'paid') this.setState({
success: true,
checkoutData: checkoutData
});
}
render() {
if (this.state.success) {
const customerName = this.state.checkoutData.shipping.name.split(' ')[0];
const paidAmount = (this.state.checkoutData.amount / 100).toFixed(2);
const shipTo = this.state.checkoutData.shipping.address.line1 + ', ' + this.state.checkoutData.shipping.address.city;
return <CheckoutSuccess customerName={customerName} paidAmount={paidAmount} shipTo={shipTo} />
}
return <CartView productSku='sku_CWxpCQ9W50WpNR' />;
}
}
export default CheckoutFlow;

To implement the React/Stripe backend and for more info on using React with Stripe, checkout Robin Wieruch’s blog post I mentioned earlier. My code is basically the same, however I will copy it below so you can see it.

There are three constants folders that hold the constants required by the other files:

// /constants/frontend.jsconst FRONTEND_DEV_URLS = [ 'http://localhost:5000' ];const FRONTEND_PROD_URLS = [
'https://www.habitsocks.com',
'https://habitsocks.com'
];
module.exports = process.env.NODE_ENV === 'production'
? FRONTEND_PROD_URLS
: FRONTEND_DEV_URLS;
// /constants/server.jsconst path = require('path');const SERVER_PORT = 8080;const SERVER_CONFIGS = {
PRODUCTION: process.env.NODE_ENV === 'production',
PORT: process.env.PORT || SERVER_PORT
};
module.exports = SERVER_CONFIGS;// /constants/stripe.jsconst configureStripe = require('stripe');const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET;const stripe = configureStripe(STRIPE_SECRET_KEY);module.exports = stripe;

Next are the routes, or how the server deals with what urls are accessed. In payment.js the app.post() method differs significantly from the aforementioned blog post. That is because an ‘order’ needs to be created and then paid to capture a users email and track what they bought etc. This is simple enough as can be seen below.

// /routes/payment.jsconst stripe = require('../constants/stripe');const postStripeCharge =  (res, err, order) => {
if (err) {
res.status(500).send({ error: err });
} else {
res.status(200).send({ success: order });
}
}
const paymentApi = app => {
app.get('/', (req, res) => {
res.send({ message: 'Hello Stripe checkout server!', timestamp: new Date().toISOString() });
});
app.post('/', (req, res) => {
stripe.orders.create(req.body.order, (err, order) =>
stripe.orders.pay(order.id, req.body.pay, (err, order) => postStripeCharge(res, err, order)));
});
return app;
};
module.exports = paymentApi;// /routes/index.jsconst paymentApi = require('./payment');const configureRoutes = app => {
paymentApi(app);
};
module.exports = configureRoutes;

Finally, the server configuration file is very important for our app. Here I use CORS to allow requests to be made from other origins. Otherwise our requests from the frontend server would be blocked.

// /server.jsconst cors = require('cors');
const bodyParser = require('body-parser');
const CORS_WHITELIST = require('./constants/frontend');const corsOptions = {
origin: true
};
const configureServer = app => {
app.use(cors(corsOptions));
app.use(bodyParser.urlencoded());
};
module.exports = configureServer;

The index.js file which is just the entry point for the application. It loads everything and serves it up.

// /index.jsconst express = require('express');const SERVER_CONFIGS = require('./constants/server');const configureServer = require('./server');
const configureRoutes = require('./routes');
const app = express();configureServer(app);
configureRoutes(app);
app.listen(SERVER_CONFIGS.PORT, error => {
if (error) throw error;
console.log('Server running on port: ' + SERVER_CONFIGS.PORT);
});

5. What to do next?

Once I had payments working successfully, I setup a Zapier account that would be triggered when it detects a new order on Stripe and send an email to the user as well as adding a todo item to my Trello board. That way I could easily keep track of payments in a cheap and simple way. Maybe I will do a blog post about that in the future? Until then, I hope you enjoyed this one!

PS. All files (including stylesheets) can be found on my GitHub here (for frontend) and here (for backend). Feel free to check them out and use them as a guide.

Matt Hough is the founder of Tolenno, a website that helps you find a home that is healthy. Check it out at tolenno.com.

--

--

Matt Hough

Working on technology that can benefit the world. Founder of Hype Servers and Tolenno. Learning full stack web development at Flatiron School and public health.