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:
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.
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.
We have now added
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:
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:
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:
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.
Here we can filter the user_id column by making use of
session variables that comes from the token.
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.
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:
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.
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
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
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:
Again note that, permissions can be applied declaratively to ensure the cart item is tagged to the right user and cannot be manipulated.
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:
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.
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.
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.
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
On the frontend, GraphQL subscriptions can be used to track the payment status in realtime. The subscription is very straightforward:
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.
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.