Strapi Ecommerce Product Variations Generator

April 28th 2020

Strapi.io is a widely popular Headless CMS that continues to grow in popularity. If you’ve read some of our other blogs such as Migrating a WordPress Blog to Strapi you know that we are bullish on the Strapi platform.

We recently worked with a company to create rustic.co which is a an ecommerce website specializing in rustic furniture and decor. As with most ecommerce websites, products on the site will come in many different types of variations. Take a t-shirt for example. You might have a t-shirt that could have a size large, medium or small and a color red, green or blue. This would mean that your t-shirt product could have nine different variations to choose from. It would be very time consuming to create nine different variations for each type of t-shirt that you loaded onto your store. In true fashion of a nifty programmer we could ask ourselves “how can we automate this process”?

Product Variations Generator

Some of the other headless content management system solutions that focus on ecommerce such as moltin commerce provide an endpoint that exposes a generator which automates and creates product variations related to our product. Since Strapi is a n API first solution, we can create the same type of generator for our Strapi ecommerce website that other platforms provide.

The Product Content Type

We will assume that you have a Strapi installation ready and installed on your local machine. If not, you can jump over to the Strapi Quick Start Guide to quickly setup your strapi installation. You can select your database of choice during the installation process, as the database structure you choose will not affect our generator. Once installed, you can start your development server and create an admin user by navigating to http://localhost:1337/admin. Once you are logged in we can begin to create the Content Types for our variation generator.

First, navigate to the Content-Types Builder plugin found in the left hand navigation menu. We are going to be creating a content type called product that we will use to store our product information. Think of the product as the “parent” to each one of the variations that we will generate. The parent will define most of the initial information such as “name”, “stock”, “price” etc and each variation will inherit the initial values from the parent upon generation. Once you are on the Content-Types Builder you can click on the button “Add A Content-Type”. For the name field we can call it product making sure to follow the Strapi convention that it is singular.

Once you have filled out the Name field you can select Done. We will continue by adding our fields to the content type. First, we can add a field of type String called name which will contain the name of our product. We can save and add another String field called slug which will act as the UUID field for us. If you are using strapi beta 19 and up you can create a UUID field called slug and link it to the product name field. Next, we’ll add a decimal field called price, a decimal field called sale and finally another decimal field called stock. To give our products some content we can add a text field called description which will hold our product description and a media field called images which will contain our images for the product. Finally, and most importantly, we’ll create an attributes field of type JSON which we will use to define the attributes (and the attribute values) for our product such as “color”, “size” etc. You should now have a content type of product with field definitions:

You can save the new product type and the server will restart.

Variation Content Type

We are going to create a new content type called variation that will be used to generate product variation information. Similar to the steps for product you can create a new content type that is called variation making sure that it is singular. We are going to be adding the same fields that we have for product except for the attributes field as that will not be needed for our variation type. Repeat the steps above for all of the fields: name, slug, price, images, sale, stock, description and save the content type. Now we can create the relationship between variation and product.

Add a new field to your variation content type of type relation. Under the field name we can call it product as it’s a relationship to a single parent product. Since one product have many variations we want to make sure that the relationship is Product has many Variations is selected and then you can save the field.

With our Product and our Variation content types setup and a relationship is created between them we can turn our attention to our variation generator function. Since strapi allows you to scaffold an API we have the ability to add a route to our Product content type and define an action in our Product controller to handle the request and generate the variations relating to the product.

Product Build Endpoint Route

We’re going to create a route in our Product API definition. In your strapi project directory navigate to api/product/config and edit the file api/product/config/routes.js at the end of the routes array in the file we can add another route definition for our build endpoint such as:

{
  "routes": [
    {
      "method": "GET",
      "path": "/products/:_id/build",
      "handler": "Product.build",
      "config": {
        "policies": []
    }
  ]
}

With this get request added we are creating an endpoint that points to our Product.build controller action at the route /products/:_id/build which for example would be accessible as a GET request to http://localhost:1337/products/5dc0affc0e710c3d3660a4fc/build where the 5dc0affc0e710c3d3660a4fc is the product id of the product you want to generate variations for. Next up, we have to create a controller action build that is going to handle our variation build. In your project directory you can navigate to api/product/controllers and edit the file api/product/controllers/Product.js. Inside the the modules.export we can create a property called build that is a function definition. For now we can just set our ctx.status and return our ctx.body to test that our endpoint is working:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    ctx.status = 200
    ctx.body = 'Ok'
  }
};

We will need to open up our permissions for our new route/controller that we created. Navigate to “Roles & Permissions” in your Strapi dashboard and click on the “Public” role. Scroll down to “Product” and make sure build is checked then scroll up and click “Save”.

To test this we’ll need to first create a product in our Strapi admin dashboard. We can navigate to our Products link in the left hand sidebar and click “Add New Product”. Fill in a dummy product for now and save the product. Once created you can copy the product id from the list or click back into the product you created to copy the id. Now we can test our http://localhost:1337/products/:_id/build by using a service such as Postman to test our endpoint. In postman create a new GET request to the endpoint http://localhost:1337/products/5dc0affc0e710c3d3660a4fc/build making sure to replace your product id that you copied. You should see Postman respond with an “Ok” message and status 200. Great! Our endpoing is setup and working.

Product Build Action

We can start working through our build action function to create variations for the product route that we defined. We’re going to edit the build function definition in our api/product/controllers/Product.js file. First we’re going to make sure that the product id that we’re passing as a route parameter exists, otherwise we’ll return:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    let product = await strapi.services.product.findOne(ctx.params)
    if(!product.attributes.length) return
  }
};

Now we are going to be defining a few helper functions for our action. First, is a cartesian function which is a combination generator function that takes a single array argument containing multiple arrays of values such as [["red", "green", "blue"], ["small", "medium", "large"]] and returns an array of arrays with the combination of the values such as:

[["small","red"],["small","green"],["small","blue"],["medium","red"],["medium","green"],["medium","blue"],["large","red"],["large","green"],["large","blue"]]

The cartesian function will look like:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    let product = await strapi.services.product.findOne(ctx.params)
    if(!product.attributes.length) return

    const cartesian = (sets) => {
      return sets.reduce((acc, curr) => {
        return acc.map(x => {
            return curr.map(y => {
                return x.concat([y])
            })
        }).flat()
      }, [ [] ])
    }

  }
};

This helper function will take an array of arrays and generate the combinations we are looking for in our product variations. We are also going to add a helper function that capitalizes the first letter of every word in a string so we can use it to auto generate our variation name from the product name, appending the attribute(s) to the variation name as well. For instance, a product named T-shirt with the attributes red and large would generate a variation with the name T-shirt Red Large. Let’s add the helper function to our build action in our controller:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    let product = await strapi.services.product.findOne(ctx.params)
    if(!product.attributes.length) return

    const cartesian = (sets) => {
      return sets.reduce((acc, curr) => {
        return acc.map(x => {
            return curr.map(y => {
                return x.concat([y])
            })
        }).flat()
      }, [ [] ])
    }

    //capitalize function
    const capitalize = (s) => {
      if (typeof s !== 'string') return ''
      return s.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")
    }

  }
};

Now we will need to get the attributes field from the product that we are “building”. Remember we created a field for our Product content type that was called attributes? This field will list all of the different attributes of the product that need to be generated. An example of a T-shirt product attributes field would look like this:

[
  {
    "name": "size",
    "options": [
      {
        "value": "sm",
        "description": "small t-shirt"
      },
      {
        "value": "md",
        "description": "medium t-shirt"
      },
      {
        "value": "lg",
        "description": "large t-shirt"
      }
    ]
  },
  {
    "name": "color",
    "options": [
      {
        "value": "red",
        "description": "burnt red t-shirt"
      },
      {
        "value": "blue",
        "description": "sky blue t-shirt"
      },
      {
        "value": "green",
        "description": "hunter green t-shirt"
      }
    ]
  }
]

This JSON array lists our two different attributes which are size and color and all of the options available. Now we can save this to our product so we’re able to get the attributes field in our generator function. We can then use our cartesian helper function to map over the product’s attributes field creating an array of all of our variations:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    let product = await strapi.services.product.findOne(ctx.params)
    if(!product.attributes.length) return

    const cartesian = (sets) => {
      return sets.reduce((acc, curr) => {
        return acc.map(x => {
            return curr.map(y => {
                return x.concat([y])
            })
        }).flat()
      }, [ [] ])
    }

    //capitalize function
    const capitalize = (s) => {
      if (typeof s !== 'string') return ''
      return s.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")
    }

    const { attributes } = product

    //map functions return an array of array [["sm", "md", "lg"], ["red", "green", "blue"]]
    //cartesian function reduces and combines arrays and returns mixed variations
    //[ [ { size: 'sm' }, { color: 'blue' } ], [ { size: 'sm' }, { color: 'red' } ], [ { size: 'sm' }, { color: 'green' } ], [ { size: 'md' }, { color: 'blue' } ], [ { size: 'md' }, { color: 'red' } ], [ { size: 'md' }, { color: 'green' } ], [ { size: 'lg' }, { color: 'blue' } ], [ { size: 'lg' }, { color: 'red' } ], [ { size: 'lg' }, { color: 'green' } ]]
    const variations = cartesian(_.map(attributes, ({name, options}) => _.map(options, ({value}) => ({ [name]: value }) )))

  }
};

Now we have a variations variable that holds all of our variation combinations for the product that we are building. We can now move on to create an array of variation “records” that will copy over the rest of the product fields for each variation such as: name, slug, price, sale, stock, description. A few of these fields will be adjusted such as the slug field because we want that to be unique to our variation and do not want an exact duplicate of the product field. The best was to create an array of records is to map over the variations variable we created, returning a record object for each variation we need generated such that:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    ...
    const variations = cartesian(_.map(attributes, ({name, options}) => _.map(options, ({value}) => ({ [name]: value }) )))

    //iterate through all variations creating the records
    const records = _.map(variations, variation => {
      let name = variation.reduce((acc, current) => acc + " " + Object.values(current)[0], product.name)
      let slug = variation.reduce((acc, current) => acc + "-" + Object.values(current)[0].replace(/ /g, '-'), product.slug).toLowerCase()

      return {
        product: product._id,
        name: capitalize(name),
        slug: slug,
        price: product.price,
        description: product.description,
        stock: product.stock,
        ...('sale' in product && { sale: product.sale }),
      }
    })

  }
};

You can see we are creating a relationship here by associating a product property on our variation record with the product._id which will create the “parent to child” relationship. We’re also using our capitalize helper function to capitalize the name which is a reduced string of our variation array concatenating the product name with the variation such as T-shirt Large Red. The records variable will hold an array of variation records that we can use to save to our Strapi database. Let’s look at how we would use a strapi service to save those records.

Saving Product Variation In Strapi

Strapi exposes services to each of the content types you create to help with CRUD (create, read, update and delete) operations for entries to the database.

When you create a new Content Type or a new model, you will see a new empty service has been created. It is because Strapi builds a generic service for your models by default and allows you to override and extend it in the generated files

https://strapi.io/documentation/3.0.0-beta.x/concepts/services.html#core-services

The service we are interested in is the create service that we can access via strapi.services.variation.create which accepts an object that represents the variation that we want to create. The create service returns a promise which resolves the created entry in the database. Since the service returns a promise we can await the resolution of the promise to get our created entry object. Since we are going to be awaiting multiple promises we can use Promise.all to await all of the promises for that we will be creating using our records array. Let’s look at what that might look like:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    ...
    try{
      const createAllRecords = await Promise.all(records.map( record =>
        new Promise( async (resolve, reject) => {
          try{
            const created = await strapi.services.variation.create(record)
            resolve(created)
          }catch(err){
            reject(err)
          } 
        })   
      ))
      ctx.send(createAllRecords)
    }catch(err){
      console.error(err)
    }
  }
};

We are using Promise.all to await all of our variation records being created using our strapi.services.variation.create(record) service. Once all of the entries are created we’re able to use ctx.send(createAllRecords) to return the variations that have been created. Putting it all together the build action in our Product.js controller should look like:

'use strict';

/**
 * Read the documentation () to implement custom controller functions
 */

module.exports = {
  build: async ctx => {
    let product = await strapi.services.product.findOne(ctx.params)
    if(!product.attributes.length) return

    const cartesian = (sets) => {
      return sets.reduce((acc, curr) => {
        return acc.map(x => {
            return curr.map(y => {
                return x.concat([y])
            })
        }).flat()
      }, [ [] ])
    }

    //capitalize function
    const capitalize = (s) => {
      if (typeof s !== 'string') return ''
      return s.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")
    }

    const { attributes } = product

    const variations = cartesian(_.map(attributes, ({name, options}) => _.map(options, ({value}) => ({ [name]: value }) )))

    //iterate through all variations creating the records
    const records = _.map(variations, variation => {
      let name = variation.reduce((acc, current) => acc + " " + Object.values(current)[0], product.name)
      let slug = variation.reduce((acc, current) => acc + "-" + Object.values(current)[0].replace(/ /g, '-'), product.slug).toLowerCase()

      return {
        product: product._id,
        name: capitalize(name),
        slug: slug,
        price: product.price,
        description: product.description,
        stock: product.stock,
        ...('sale' in product && { sale: product.sale }),
      }
    })
    // try creating all of our records
    try{
      const createAllRecords = await Promise.all(records.map( record =>
        new Promise( async (resolve, reject) => {
          try{
            const created = await strapi.services.variation.create(record)
            resolve(created)
          }catch(err){
            reject(err)
          } 
        })   
      ))
      ctx.send(createAllRecords)
    }catch(err){
      console.error(err)
    }
  }
};

We have created our build action for our Product content type and have exposed an endpoint at http://localhost:1337/products/:_id/build in which we can use this endpoint to auto generate product variations. Now you can open Postman or similar utility and send a GET request to http://localhost:1337/products/5dc0affc0e710c3d3660a4fc/build making sure you replace your respective product id and you should be returned an array of variation entries that have been generated for you. In the strapi admin dashboard you can navigate to Variations under the Content Types in the left hand sidebar to see the auto generated variations that have been created by our endpoint! Of course, if you need to edit any of the variations independently you can use the admin GUI to adjust any of the variation data, but we’ve saved a lot of work by auto generating all of the variations.

We have created Product Variations generator to create multiple variations for each product that have multiple attributes to save time and keep track of product variations added to our cart for ecommerce orders. Until next time, stay curious, stay creative!