Sign in
Log inSign up
Using Strapi for Node.js Content Management with a React SPA

Using Strapi for Node.js Content Management with a React SPA

Michael Poirier-Ginter's photo
Michael Poirier-Ginter
·Jul 15, 2019

A few months ago, we summoned our in-house Node.js expert to craft a piece about the state of its ecosystem.

In it, he never mentioned Strapi.js.

What a fool. 🤦‍♂️

Sure enough, our readers put him back on track by suggesting this powerful Node.js API framework in the comments.

Today, to prove we took this feedback to heart, I'll make good use of Strapi. I'll show you how to handle content management in Node.js with a React single-page application.

Steps:

  1. Setting up a Node.js backend for Strapi with MongoDB.
  2. Creating appropriate content types in Strapi for the SPA.
  3. Building a React SPA (a basic online shop) with create-react-app.
  4. Bundling & deploying the Universal JS app!

In the process, I reflected a lot about this kind of Universal JavaScript stack and its place in the developer's space.

So let's get philosophical a bit before jumping into technical stuff.

Node.js with React: Why go the Universal JavaScript way?

Only a few years back, building a full stack JavaScript app was pure fantasy. But things have changed.

node-js-react-stack

Some might call it Isomorphic JavaScript, but there seems to be a consensus around the term Universal JavaScript. So I'm sticking with the latter here.

It all started with the MEAN stack (MongoDB/Express/Angular/Node)—the first go-to JS full stack. You can still opt for it today, but it would be a mistake to think it's the only available stack.

<span class="share-quote" data-url="snipcart.com/blog/node-js-react-strapi-tut…" data-hashtags="#react #nodejs" data-log>The vastness of the modern JavaScript ecosystem allows you to mix the puzzle pieces and get to your perfect picture.</span>

Once you've found your ideal stack, benefits of Universal JS abound:

  • Code universality: Uniting your stack instead of dividing it. Sharing code between the frontend and backend reduces code duplication to the bare minimum—hence, easier maintenance.
  • Performance & SEO boost: with Universal JavaScript, you can build a web app rendering seamlessly on both server and browser.
    • npm install: npm, the largest software registry, lets you easily install everything on the backend and frontend.
  • Overall great UX: Important parts of a page are rendered on the server and shown to users rapidly. Other elements can be rendered client-side, after these essential parts are loaded.

Plus, the choice of (excellent) tools to work with is wider than ever.

Options for your React frontend ⚛️

Here's what the React ecosystem has to offer for building a great SPA:

Gatsby

What started as a simple static site generator has evolved way beyond that. You can use it to create progressive web apps, or fetch data with GraphQL. Its founder wants to change the "static" game, so keep an eye on it!

Next.js

Next.js is a lightweight framework for static and server‑rendered applications. Its cool set of features include automatic code splitting, simple client-side routing, webpack-based dev environment and easy implementation with any Node.js server. You can also use it as a static site generator.

For this demo's basic needs, I'll generate the app using create-react-app. It enables app creation with no build configuration, allowing to focus on relevant code.

Options for your Node.js backend 🖥️

Many server-side frameworks also spawned with the rise of Node.js:

Express

Express remains the most widely used of them. It's a fast, unopinionated, minimalist web framework for Node.js. Its straightforward approach is probably what comes closer to Node.js' basic idea of a lightweight system with a modularity approach.

Koa.js

What happens when you take Express and strip it down to its most simple form? Koa.js. The same team behind the former built it as a smaller, more expressive and robust foundation for web apps and APIs. It's marketed as the next generation web framework for Node.js and you should check it out.

Sails.js

Sails is a real-time, MVC framework. It was designed to emulate the MVC pattern of Ruby on Rails but with support for modern apps. It does this through data-driven APIs with a scalable, service-oriented architecture.

Then there's Nest.js, Socket.io, MEAN.js and, of course, the youngest of the bunch: Strapi.

What is Strapi?

Strapi is a Node.js API framework with headless CMS capabilities.

stapi-nodejs-backend

It's said to be the most advanced Node.js content management framework, saving developers weeks of API configurations. We'll see about that today!

It comes with an impressive set of features (including an admin panel, powerful CLI, fast & secure) and useful plugins.

More importantly for my use case is that it's frontend agnostic. It means that it connects to the frontend framework of your choice, as well as mobile apps and even IoT (Internet-of-Things).

Looks like it checks all the boxes for a neat Node.js with React stack.

It made quite a splash in its short lifetime, and the word-of-mouth is pretty good around it, so I'm excited to try it!


Strapi tutorial: Node.js content management for React.js app

node-js-react-strapi-tutorial

Prerequisites

  • A database for your project (Strapi strongly recommends MongoDB)
  • Basic understanding of single-page applications (SPA)
  • If you want to enable e-comm. functionalities, a Snipcart account (forever free in Test mode)

1. Getting Started with Strapi

Let's set up your development environment. For Strapi, this is pretty easy, install it using NPM:

npm install strapi@alpha -g

Now create a new project in the location of your choice using Strapi's CLI

strapi new strapi-project

Follow the on-screen instructions to link Strapi with your database.

1.1 Starting the server

Once the project is generated, go into the project's directory and start the Node.js server.

cd .\strapi-project\
strapi start

You'll now be able to reach the administration panel through the following URL: localhost:1337/admin.

1.2 Creating a Strapi user

On the administration page, you'll be prompted with a welcome message and a form to create a root user. Once again, follow the onscreen instruction.

create Strapi user

1.3 Crafting new content types

Now that you have access to a root user you can proceed to make a new content type. In Strapi, the API is built alongside the creation of new content types.

Since this demo is about building a simple e-commerce app, I'll create a product and add fields matching Snipcart's product definition.

strapi-create new content type

Strapi acts as an interface between your database and API. This is why a new collection called product will appear in the database.

1.4 Making an entry for your content type

Now that your content type is set up, make a new entry for your product. Strapi will insert a new document in the product collection in your database.

strapi-create new entry

1.5 Grant access

By default, Strapi restricts the access of new content types actions to the administrator only.

To address this, you can go into "Roles & Permission" panel and add the find and findone permission of your product content type into the public role.

strapi-grant acess

There you go!

With Strapi, that's all it takes to create an underlying API that your frontend will be able to access and load data from. If you visit localhost:1337/product you should be greeted with a list of all your products.

You can also visit localhost:1337/:_id (where :_id is the id of your product) and should receive a JSON containing only the specified product.

2. Setting up the React application

Now that your backend is nearly finished, let's create a single-page app.

npx create-react-app react-app
cd react-app
npm start

Once completed, you can visit the app at the following URL: localhost:3000

2.1 Adding SCSS

If you wish to use traditional CSS or any other styling method, this part is not required. However, since I wanted to use SCSS over CSS, I needed to add a new loader to my project.

To do so, you'll need to decouple the project from the create-react-app tooling and install the sass-loader package.

Keep in mind that this is a permanent and non-reversible operation.

Eject the project and install sass loader:

npm run eject
npm install sass-loader node-sass --save-dev

This action will notably expose webpack's configuration files inside a directory called config.

Open up webpack.config.dev.js, webpack.config.dev.js and add the following code inside the module's rules array:

test: /\.scss$/,
use: [
    "style-loader", // creates style nodes from JS strings
    "css-loader", // translates CSS into CommonJS
    "sass-loader" // compiles Sass to CSS
    ]
}

This way, you can import any SCSS files inside a component just like you would with a regular CSS file. For instance, in my App.js component, I've imported its appropriate stylesheet.

import './styles/app.scss';

Quick tip while you are configuring everything: in the earlier days of React, if you wanted to use React's preprocessor, every file required the .JSX extension. However, this is not the case anymore since Babel and project files now use the regular .js extension.

The issue with this kind of setup is that your text editor or IDE will most likely not be able to tell the difference between both syntaxes.

If you're using VSCode (and you should!), you can fix this by adding the following snippet in your workspace settings:

"files.associations": {
    "*.js": "javascriptreact"
}

It will come in helpful when auto-indenting or auto-completing HTML within the JavaScript code.

2.2 Generating ProductList component

Like most modern frontend libraries or framework, React uses components. Here, you'll create two components:

  • The ProductList component will list all components.
  • The Product component will act as a view for a detailed product description.

First, generate a ProductList.js file inside a components directory with the following code:

import React, { Component } from 'react';
import { Link } from 'react-router-dom'
import BuyButton from './BuyButton';

class ProductList extends Component {
  constructor(props) {
    super(props);

    this.state = {
      loading: true,
      products: []
    }
  }

  async componentDidMount() {
    let response = await fetch("snipcart-strapi.herokuapp.com/product");
    if (!response.ok) {
      return
    }

    let products = await response.json()
    this.setState({ loading: false, products: products })
  }

  render() {
    if (!this.state.loading) {
      return (
        <div className="ProductList">
          <h2 className="ProductList-title">Available Products ({this.state.products.length})</h2>
          <div className="ProductList-container">
            {this.state.products.map((product, index) => {
              return (
                <div className="ProductList-product" key={product.id}>
                  <Link to={`/product/${product.id}`}>
                    <h3>{product.name}</h3>
                    <img src={`https://snipcart-strapi.herokuapp.com${product.image.url}`} alt={product.name} />
                  </Link>
                  <BuyButton product={product} />
                </div>
              );
            })}
          </div>
        </div>
      );
    }

    return (<h2 className="ProductList-title">Waiting for API...</h2>);
  }
}

export default ProductList;

In React, each component holds its own state and takes creation parameters called props. This is why inside your constructor, you'll need to pass your props to its parent class and set the state of your component as a JavaScript object with a loading key and a products key.

Every time the state of a component is modified, the component will be re-rendered using the render() method.

componentDidMount() is a function that's called every time it's mounted. Fetch your products using the /product endpoint of the API inside this function.

Add the response's data inside the state of your component as well as update the loading key to false as you've completed the fetch.

Inside your Render() method, you'll have two potential returns. The first one is called if the loading value of the state is false, the other if it's true. This will prevent rendering your products if you haven't fetched the item yet.

In case the loading is finished, you'll map the products from your API response to print out each product as well as rendering a buy button. The BuyButton is a separate component that takes the product as a prop to create its state.

import React, { Component } from 'react';

class BuyButton extends Component {
    constructor(props) {
        super(props);

        this.state = {
            id: props.product.id,
            name: props.product.name,
            price: props.product.price,
            weight: props.product.weight,
            description: props.product.description,
            url: "snipcart-strapi.herokuapp.com/snipcartParser"
        }
    }

    render() {
        return (
            <button
                className="snipcart-add-item BuyButton"
                data-item-id={this.state.id}
                data-item-name={this.state.name}
                data-item-price={this.state.price}
                data-item-weight={this.state.weight}
                data-item-url={this.state.url}
                data-item-description={this.state.description}>
                ADD TO CART ({this.state.price}$)
            </button>
        );
    }
}

export default BuyButton;

The link tag is imported into the react-router-dom package that will allow you to switch between different views. More about that later on.

2.3 Creating Product component

The product component will follow the same principles as the ProductList, but will be displayed when trying to view an individual product.

this.props.match.params.id is the value of the id that passed in the URL. I'll explain this in the next section.

import React, { Component } from 'react';
import BuyButton from './BuyButton';

class Product extends Component {
  constructor(props) {
    super(props);

    this.state = { loading: true, product: {} }
  }

  async componentDidMount() {
    let response = await fetch(`snipcart-strapi.herokuapp.com/product${this.props.match.params.id}`)
    let data = await response.json()
    this.setState({
      loading: false,
      product: data
    })
  }

  render() {
    if (!this.state.loading) {
      return (
        <div className="product">
          <div className="product__information">
            <h2 className="Product-title">{this.state.product.name}</h2>
            <img src={`https://snipcart-strapi.herokuapp.com/${this.state.product.image.url}`} />
            <BuyButton {...this.state} />
          </div>
          <div className="product__description">
            {this.state.product.description}
          </div>
        </div>
      );
    }

    return (<h2>Waiting for API...</h2>);
  }
}

export default Product;

2.4 Routing the app

Now, time to turn the app into an SPA and leverage the new components. To do so, install React Router, which is broken down into multiple packages.

You'll only need the following:

npm install --save react-router
npm install --save react-router-dom

react-router will provide the core functionalities for React Router while react-router-dom will contain all the browser related functionalities.

Inside the index.js file, wrap your app component inside a BrowserRouter.

import { BrowserRouter } from 'react-router-dom'

ReactDOM.render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'));
registerServiceWorker();

Now, inside the App component, import Route and Switch from react-router and link from react-router-dom:

import { Route, Switch } from "react-router";
import { Link } from 'react-router-dom'

This will allow you to do the following inside the render of your App:

<Switch>
  <Route path="/" exact component={ProductList} />
  <Route path="/product/:id" component={Product} />
</Switch>

Route renders the view of the appropriate component depending on its path.

Switch acts just like a regular switch case and allows you to render a maximum of one of its route. The exact attribute will render the route only if the path is exact.

In this setup, the switch in unnecessary since I used the exact tag and I only have one more route. But in case you add more routes, leave it wrapped inside a Switch.

:id is a placeholder that will be able to retrieve the real value inside the product component using props.match.params like you did earlier.

This should leave us with something like this:

import React, { Component } from 'react';
import ProductList from './components/ProductList';
import { Route, Switch } from "react-router";
import { Link } from 'react-router-dom'
import Product from './components/Product';
import './styles/app.scss';


class App extends Component {
  render() {
    return (
      <div className="App">
        <Header />
        <main className="App-content">
          <Switch>
            <Route path="/" exact component={ProductList} />
            <Route path="/product/:id" component={Product} />
          </Switch>
        </main>
        <Footer />
      </div >
    );
  }
}

In this snippet, Header and Footer are simple JavaScript functions defined below the app that return DOM tags.

const Header = () => {
  return (
    <header className="App-header">
      <Link to="/"><h1>🦐 Exotic Fish Supplies</h1></Link>
      <div className="right">
        <button className="snipcart-checkout snipcart-summary">
          Checkout (<span className="snipcart-total-items"></span>)
      </button>
      </div>
    </header>
  );
}

You can opt for this kind of syntax rather than use ES6 classes when your components don't require lifecycle methods.

3. Integrating a shopping cart

To add Snipcart into your website simply include it inside the head tag of your index.html file located inside the public directory.

<script src="ajax.googleapis.com/ajax/libs/jquery/2.2.2…"></script>
<script src="cdn.snipcart.com/scripts/2.0/snipcart.js" data-api-key="MDk5ZGFlMzEtZjVjMy00OTJkLThjNzEtZjdiOTUwNTQwYWMwNjM2Njg1ODA5NTQzMTIzMjc0" id="snipcart"></script>
<link href="cdn.snipcart.com/themes/2.0/base/snipcart.…" rel="stylesheet" type="text/css" />

3.1 Writing a custom route using Strapi

Lastly, you'll need to create a route retrieving all products in a format valid with Snipcart's JSON crawler.

Although Strapi doesn't allow you to filter specific fields from the response unless you have GraphQL in your stack, you can create a custom route that will do this job for you.

Open routes.json in Strapi's product configuration directory (/api/product/config) and add the appropriate route.

{
    "method": "GET",
    "path": "/snipcartParser",
    "handler": "Product.snipcartParser",
    "config": {
    "policies": []
}

Since Strapi uses an MVC approach, you'll also need to make the appropriate changes in the controller. Go in the controllers directory and add the following:

/**
 * Retrieves all the products with fields valid with Snipcart's JSON crawler.
 *
 * @return {Object}
 */

snipcartParser: async (ctx) => {
    let products = await strapi.services.product.fetchAll(ctx.query);
    return products.map(product => {
        return {
        id: product._id,
        price: product.price,
        url: "snipcart-strapi.herokuapp.com/snipcartParser"
        }
    })
}

This function will fetch the products just like the /product route would, but extract only the fields required by Snipcart.

Once this is done, you can restart your server and add the appropriate permission to the new route inside the product content type.

GitHub repo & live demo

Node.js with React live demo

See GitHub repo here

See live demo here

Closing thoughts

I truly enjoyed building this demo. Strapi is a breeze to use, and I felt a little more at home using React as a frontend framework than I did with Angular.

Furthermore, I loved Strapi's approach. It's simple enough to do most of what you need to do out of the box; still, it's customizable enough to fit in most use cases as demonstrated when creating a custom route.

I spent less than two days building this demo application. Although Strapi is easy to use, you're not always a Google search away of solving your problems since it's still a relatively new tool. Thankfully, the creators are pretty active in the community, and it's just a matter of time before most issues get adequately documented.

Even though putting together this integration gave me a good overview on what's available in Strapi; there are a few concepts that have been untouched. For instance, I would've liked to experiment with the policies concept built-in Strapi to create an authentification system of some sorts.


If you've enjoyed this post, please take a second to share it on Twitter

Originally published on the Snipcart blog and in our newsletter.