All Ruby on Rails Node JS Android iOS React Native Frontend Flutter QA

Fashion Store With GraphQL

About GraphQL

GraphQL is a query language and runtime engine. A query language describes how one can communicate with an information system. A runtime is responsible for returning data for queries. GraphQL was developed as an alternative to REST. The difference is that in REST, we collect data from many endpoints, while each endpoint always returns the same set of data. In contrast, GraphQL uses one endpoint that only returns the necessary data each time. GraphQL provides data in a predefined schema. The schema defines what queries are allowed and what types of data can be fetched. Knowing the schema before querying, the client can be sure the server will be able to respond to the query, and the client can predict what will be returned. One does not need to fetch the entire schema – you can fetch only the selected fragment. GraphQL also has some limitations, for instance, it is difficult to cache data and transfer file-like data. 

In GraphQL, we have three types of operations:

  • Queries for fetching data
  • Mutations for writing data
  • Subscriptions for receiving real-time data

Fashion store 

There are a bunch of actions related to searching and ordering products that are used to create online store experience. GraphQL could make this store faster and simpler to create and maintain. That's why we want to check if we can easily implement all the features needed in this type of store using Rails and GraphQL together.

Browsing products 

The basic functionality in a shop is browsing products. We should be able to browse all products or show only products in a specified category.  The system must fetch information about each product, about their parameters, available variants, colours, and sizes. GraphQL can list specified data and gather information from related models, e.g. information about a product category. We can create it using GraphQL queries. We can provide the schema with all information about a product. You can change the amount of data fetched by the frontend without making any modifications to the backend. In Rails, we can do it by declaring a specified type for products.

class ProductType < Types::BaseObject
   field :id, ID, null: false
   field :name, String, null: false
   field :price, Integer, null: true
   field :description, String, null: true
   field :product_category, Types::ProductCategoryType, null: true
   field :product_variants, [Types::ProductVariantType], null: true
 end

Besides data fields, this type also contains relations to other types that can provide additional information about the product. Then, we should add a field with this type to the query type to make it accessible directly from the query. We can use the same type for one product and for listing all products. 


module Types
 class QueryType < Types::BaseObject
   field :products,
         [Types::ProductType],
         null: false,
         description: "Returns a list of products in fashion store"
 
   field :product,
         Types::ProductType,
         null: false,
         description: "Return product by ID" do
           argument :id, ID, required: true
         end
 
   def products
     Product.all
   End
 
   def product(id:)
     Product.find(id)
   end
 end
end

We define similar types for other models. This approach allows us to easily determine the category to which the product being viewed belongs. 

Using GraphQL, we do not have to download all the product information when displaying the list, we can only do it when displaying the selected product. Queries are created using the special GQL syntax which specify the data we want to fetch. 

All information on the available data is included in the schema, which is self documented. Frontend devs can explore information about the schema and data types without looking at the backend code by using the GraphiQL tool. This is helpful for creating and testing queries.

Untitled-2

Search products

In our store, we can search for products by name or description. It is therefore necessary to implement this in our backend. In GraphQL, we can add filters to each defined type. For this, you need to create a separate filter class. 

class ProductFilter < ::Types::BaseInputObject
   argument :OR, [self], required: false
   argument :name_contains, String, required: false
   argument :description_contains, String, required: false
   argument :price_min, Integer, required: false
   argument :price_max, Integer, required: false
 end

Inside the filter, we define arguments that will be used to determine how to filter the downloaded data. The parameter defines the type of data that is used to filter the downloaded data. Then we determine how the data will be filtered.

def normalize_filters(value, branches = [])
   scope = Product.all
   scope = scope.where('name LIKE ?', "%#{value[:name_contains]}%") if value[:name_contains]
   scope = scope.where('description LIKE ?', "%#{value[:description_contains]}%") if value[:description_contains]
   scope = scope.where('price_cents >= ?', "#{value[:price_min].to_money.cents}") if value[:price_min]
   scope = scope.where('price_cents <= ?', "#{value[:price_max].to_money.cents}") if value[:price_max]

Determining the minimum and maximum price can be done in exactly the same way. In this case, we add parameters specifying the maximum and minimum price.

In most cases, in an application such as a store, we do not want to immediately load all products, because that would exert unnecessary pressure on loading performance. Pagination can also be done using graphQl. To this end, we modify the filter class by adding :first and :skip options. These options allow you to limit the number of records being loaded. The same approach can be helpful to display the next and previous product in the selected category.

 option :first, type: types.Int, with: :apply_first
 option :skip, type: types.Int, with: :apply_skip
 
 def apply_first(scope, value)
   scope.limit(value)
 end
 
 def apply_skip(scope, value)
   scope.offset(value)
 end

Adding products to cart and purchasing 

To make any changes to the database, e.g. add a product to the cart, you have to create a mutation responsible for the given action. In the `MutationType` class we have to add fields corresponding to the mutation we create.

class MutationType < Types::BaseObject
  field :create_cart_item, mutation: Mutations::CreateCartItem
end

Next, we create controller-like mutation classes responsible for performing individual actions. In each such class, we specify the arguments needed to be provided to perform a specific action.

class UpdateCartItem < BaseMutation
   argument :product_id, Integer, required: false
   argument :quantity, Integer, required: false
   argument :product_variant_id, Integer, required: false
end

resolve is our action method. We can implement here all the logic necessary to perform our mutation.  

def resolve(product_id:, product_variant_id:, quantity:)
  cart_item = Cart.first.cart_items.new(product_id: product_id,
                                           product_variant_id: product_variant_id,
                                           quantity: quantity)
  if cart_item.save
    {
      cart_item: cart_item,
      errors: []
    }
  else
    {
      cart_item: nil,
      errors: cart_item.errors.full_messages
    }
  end
end

In a similar way, we can perform actions such as changing the number of products in a cart or ordering products.

Conclusions

GraphQL is suitable for applications such as our store, where we have products that we want to view, which mostly reference to one parent relation such as product categories or the current user’s cart. We can provide all the necessary data easily and in a way legible to frontend developers.

Further reading 

We're building our future. Let's do this right - join us
Looking for new opportunities? Check our offers!
READ ALSO FROM Ruby
Read also
Need a successful project?
Estimate project or contact us