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:
- Setting up a Node.js backend for Strapi with MongoDB.
- Creating appropriate content types in Strapi for the SPA.
- Building a React SPA (a basic online shop) with
create-react-app
. - 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.
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.
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
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.
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 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.
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.
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
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.