Fashion Store With 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.

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.