Building a customizable e-commerce backend with Hasura

Building an e-commerce app often requires a solid backend with data integrity and high performance for low latency browsing. There are plenty of APIs to worry about and manage in the whole process. Cross platform development makes it tricky with different data requirements in different platforms (i.e web, mobile)

In this post, we will dive into building a robust e-commerce backend with all fundamental features like auth, search, inventory validations, add to cart, order placement and payments while also maintaining security.

We want to be writing the least amount of code possible without compromising the  ability to scale when required and maintaining data integrity. Our choice of technologies are very simple and powerful:

  • Database: PostgreSQL (Solid relational database to store and access data)
  • APIs and Hasura Cloud: Hasura Cloud offers hosted GraphQL Engine projects with extra features for reliability and security and Hasura GraphQL APIs for the most boring CRUD stuff and robust Authorization
  • Custom Logic: A simple Node.js server for auth, cart and order placements. (You can replace this with the language/framework of choice)

Now let's jump into the database schema.

Data modelling for e-commerce

We are using PostgreSQL for datastore. We have a bunch of tables with the right relationships setup.

A simplified version of the data model will look something like this:

PostgreSQL data model for e-commerce app
PostgreSQL data model for e-commerce app

The primary user workflow on the frontend would be:

Browse products -> Add product to cart -> Place order -> Make Payment.

Throw in signup/login anytime before placing an order to complete the workflow.

Alright, now let's break down the core components of an e-commerce app:

  • Products listing with search
  • Product page
  • User authentication
  • Cart page and Add to Cart
  • Coupon code validations
  • Place order and make payment
  • Send email to customers

Setting up a Cloud project

Follow the getting started docs to have an instance of Hasura up and running in no time.

Data fetching APIs

Let's look at the most commonly used API in the app - fetching product(s) from the database. Mix in all those fancy filters, pagination and you are looking at multiple endpoints or conditional fetching already.

Here's what the simplest of query to fetch products looks like with Hasura GraphQL.

GraphQL query for fetching products
GraphQL query for fetching products

The above query is fetching 10 products (and their metadata like stock availability). Now you can expand this query to have as much information you want in your product listing page.

Filtering and sorting

Obviously you would want to give users the ability to filter and sort the products on the UI. In the query above, you can add more arguments to achieve the same.

GraphQL query with input arguments
GraphQL query with input arguments

We have now added where and order_by arguments. If you have used SQL before, this would be very familiar territory :) You can use the same query to fetch one product at a time based on product_id in the filter. That would take care of the product detail page. So easy isn't it?

Product Search API

Searching across the product table can be implemented using Postgres functions. We can include what columns in the table we need to match the search term against. For example, you can write a function that looks like:

Postgres function to search products
Postgres function to search products

This lets you check for similarity of the given search term relative to name, description and category. Obviously you can include other product metadata to search for. Note that the similarity function and <% operators are available with the pg_trm extension. Read more about how to use pg_trm.

Now with Hasura, you can query the postgres function like:

GraphQL query for postgres function
GraphQL query for postgres function

Read more about Postgres Search here.

With the right usage of input bounce effect, you can build a solid search functionality using built-in Postgres features. But in case you are worried about latency and do not want the primary database to be hit for search operations, you can also make use of external services like Algolia with drop-in frontend code integrations.

Let us know in the comments of other similar SaaS APIs for search :)

Fetch cart items

Another primary data fetching use case is to fetch cart items. The important thing to note here is that, you should be able to filter out cart items that belongs only to the user who is browsing. This is a common permission system use case.

The query for fetching cart items would look like:

GraphQL query for fetching cart items
GraphQL query for fetching cart items

Note that we haven't applied any user filter to restrict the items that belong to the current logged in user. This is done automatically using Hasura's permission system. We can assign the customers of the app a role called user and apply check constraints declaratively like the following.

Hasura's role based permission system
Hasura's role based permission system

Here we can filter the user_id column by making use of session variables that comes from the token.

User Authentication

Next up - Authentication. The app revolves around users and their ability to login and purchase products. We have a users table in the database. The data of customers signing up can go into this table. We need a way to authenticate the user and manage sessions. With Hasura, it is very easy to set up JWT based authentication.

The backend to set up login and generate JWT tokens can be written in Node.js. Here's what the flow for a login with JWT looks like.

JWT flow - Source: Auth0
JWT flow - Source: Auth0

Now the above REST API call to our custom Node.js server can be queried via GraphQL on the same endpoint that Hasura provides.

Hasura Actions can be used to extend the auto-generated GraphQL types. Actions are a way to extend Hasura’s schema with custom business logic using custom queries and mutations.

Custom mutation for signup would look very simple:

GraphQL mutation for signup
GraphQL mutation for signup

The server generates JWT token using a custom secret shared with Hasura and returns the token back to the client after inserting the user into the database.

On the other hand, you can also make use of Auth providers like Auth0, Firebase, Cognito which works well with Hasura's Auth system.

Checkout the Hasura action - Github | YouTube

Custom Business Logic

In this section, we will be adding custom business logic for various use-cases using Hasura Actions and Postgres's built-in functions.

We got away with the auto-generated GraphQL queries for most of the data fetching. But there are some complex data validations and custom logic which cannot be automated or very specific to the app.

For example, in an e-commerce app users should be allowed to Add to cart only if there is inventory available. Similarly there is some validation required for coupon codes/discounts during order placement. Finally payments are completely external with providers like Stripe which requires validation APIs to be written on the backend.

Let's look at them case by case:

Add to Cart with Inventory Validation

Add to cart with Inventory Validation using Hasura Actions
Add to cart with Inventory Validation using Hasura Actions

On the frontend, Add to Cart flow is probably the first place you might want to enforce Authentication. Here's the built-in mutation that Hasura gives you for the cart table.

GraphQL mutation for add to cart
GraphQL mutation for add to cart

Now before adding to cart, you might want to perform validations; like check for stock availability. You can override the above Hasura mutation using a Derived Action that clones the above mutation type and lets you write custom code over this. With Action Codegen, the amount of code you will write will be very less and very focussed on the custom logic.

The new custom mutation would look like:

Custom GraphQL mutation to create cart
Custom GraphQL mutation to create cart

Again note that, permissions can be applied declaratively to ensure the cart item is tagged to the right user and cannot be manipulated.

Check out the Hasura Action - Github | YouTube

Coupon code validation

This is the step before order placement. You could provide customers with the ability to apply coupon / discount codes before making the payment.

Typically on the frontend, this would be a query to the server to check if the coupon exists and if so, what are the discount values based on various conditions.

The following custom query can be exposed to the frontend:

Custom GraphQL query to validate coupon
Custom GraphQL query to validate coupon

Check out the Hasura Action - Github | YouTube

Place order and make payment with Stripe

Order placement is the final workflow for a customer. The customer places the order by initiating the payment intent on Stripe. This step is when the banking details are entered by the customer.

Stripe payment API
Stripe payment API

Just before doing this, the frontend needs a client secret to initiate the payment with Stripe. By now, the frontend knows the order details including the currency and the address. So the frontend can trigger the following mutation with server side code written in Node.js.

GraphQL mutation for order placement
GraphQL mutation for order placement

Note that, the Node.js server gets access to all the session variables automatically to apply the right permissions. No extra code is required to conditionally check or verify if the right user is making the mutation.

Check out the Hasura Action - Github | YouTube

Realtime Payment Status

Once the payment is triggered from the frontend, it will be processed asynchronously on the backend. For example with Stripe, the frontend initiates a payment intent and server validates the payment in the backend async.

Now once the validation finishes, the Node.js function can update the payment status into a table like order_payment.

On the frontend, GraphQL subscriptions can be used to track the payment status in realtime. The subscription is very straightforward:

GraphQL subscription for order payment status
GraphQL subscription for order payment status

As soon as a row is inserted into the table, the frontend will get notified with the new data that can be shown to the user.

What about performance and scaling?

Hasura is pretty lightweight and is highly performant. The reason being that, Hasura compiles your GraphQL query to SQL with the right permissions applied and skips a lot of processing by leveraging Postgres's built-in features like JSON aggregations and Prepared statements. Read up more on Blazing fast GraphQL Execution.

Hasura has been benchmarked for upto 1 million live subscriptions and scales really well. Since you have full control over the underlying Postgres database, you will be able to optimise and fine tune by adding the right indexes and so on.

Summary

Hasura gives you most of the APIs out of the box. Combined with a robust permission system, you are actually just writing backend code pertaining to your business logic.

There are more e-commerce features like recommendation engine, support, tracking etc which we aren't covering in this post. But the takeaway is that, we have all the fundamental blocks to build anything on top or extend this very quickly with less amount of code.

The bottomline is that, for most of your data fetching requirements, be it realtime or just normal dynamic data, the out-of-the-box GraphQL APIs from Hasura is sufficient with a comforting factor that you can always quickly extend and override the GraphQL types and bring in your own API.

Additional Reading:

Blog
01 May, 2020
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.