DEV Community

Cover image for Different ways of adding your Business logic to Hasura
Vladimir Novick
Vladimir Novick

Posted on

Different ways of adding your Business logic to Hasura

Recently I taught GraphQL workshop at React Europe to 80 brilliant React developers.

Liquid error: internal

Apart from being on top of things they asked me really nice questions, one of which was "How you add your Business Logic to Hasura?". I am asked this question for quite a while, so I decided to explain different ways of how to do that.

In GraphQL workshop we've used Hasura to create our backend, and in fact, you can read more about Hasura here and follow 30 minutes hands-on course to set up your GraphQL endpoint for free along with proper authentication and authorization at learn.hasura.io. In short - production ready backend that can support 10k concurrent subscriptions, which is a pretty substantial number.

So back to our question - "How do you add your Business logic to Hasura?". Hasura not only generates GraphQL API, but it also gives you great tools to add your GraphQL server, write custom reducers or trigger serverless functions using the eventing system.

So now, let's divide our Business logic into two categories:

First, more traditional (synchronous) approach

You have several methods of adding your custom logic in a synchronous way, but high-level architecture looks like this.

Basically, it means that you write GraphQL server or custom resolvers and Hasura takes care of schema stitching.

1. Creation full blown GraphQL server.

We can use any GraphQL server because any graphql endpoint exposes type information and Hasura can easily stitch two schemas together. So to add your GraphQL server you go to Remote Schemas tab and add a new remote schema like so:

In this example, I'm adding star wars API as a remote schema. You also can see that I can pass any headers to my custom graphql server so if you have your custom authorization of graphql server endpoint, you can pass proper token or headers easily.

So what you should do in the server? Here you also have several options.
Let's assume you want direct database control. So your app is executing mutation to Hasura graphql endpoint. Hasura forwards that to your custom GraphQL server, and on your server, you write data into a database after executing your logic. The cool part is because Hasura supports subscriptions and live queries out of the box; any change that is done on the database will propagate to any client subscribed to this change.

The architecture will look like this:

If you don't want direct access, you can basically reuse the fact that Hasura generates GraphQL CRUD for you and instead of opening a connection to a database, you can simply execute GraphQL mutation from Hasura endpoint. Changing architecture to something like this:

What does it mean? It means that even if you have GraphQL server with everything already done, you still can use Hasura to provide you with an authorization layer with permissions and access control management. And you won't need to implement subscriptions or live queries because the engine already implements them.

2. Writing custom resolvers

Now, what about more granular business logic? What if you want to write dedicated resolvers. The idea is pretty much the same as custom GraphQL server, but instead of having a big schema, that will be exposed and stitched into Hasura, you will have only one resolver for your specific needs.

For example check out this resolver which is taken from Hasura learn tutorial:

const { ApolloServer } = require('apollo-server');
const gql = require('graphql-tag');
const jwt = require('jsonwebtoken');
const fetch = require('node-fetch');
const typeDefs = gql`
  type auth0_profile {
      email: String
      picture: String
    }
    type Query {
      auth0: auth0_profile
    }
`;
function getProfileInfo(user_id){
    const headers = {'Authorization': 'Bearer '+process.env.AUTH0_MANAGEMENT_API_TOKEN};
    console.log(headers);
    return fetch('https://' + process.env.AUTH0_DOMAIN + '/api/v2/users/'+user_id,{ headers: headers})
        .then(response => response.json())
}
const resolvers = {
    Query: {
        auth0: (parent, args, context) => {
          // read the authorization header sent from the client
          const authHeaders = context.headers.authorization;
          const token = authHeaders.replace('Bearer ', '');
          // decode the token to find the user_id
          try {
            const decoded = jwt.decode(token);
            const user_id = decoded.sub;
            // make a rest api call to auth0
            return getProfileInfo(user_id).then( function(resp) {
              console.log(resp);
              if (!resp) {
                return null;
              }
              return {email: resp.email, picture: resp.picture};
            });
          } catch(e) {
            console.log(e);
            return null;
          }
        }
    },
};
const context = ({req}) => {
  return {headers: req.headers};
};
const schema = new ApolloServer({ typeDefs, resolvers, context});
schema.listen({ port: process.env.PORT}).then(({ url }) => {
    console.log(`schema ready at ${url}`);
});
Enter fullscreen mode Exit fullscreen mode

As you can see it's super small, but what it does is it exports proper GraphQL schema which gets data even not from Postgres database, but from Auth0. It means that your custom resolvers can basically be hosted anywhere as soon as they expose GraphQL schema and they can do anything that you want including connecting to Microservices APIs, Third Party or just sending an email. It's up to you what to write there.

Second approach - 3factor.app architecture (asynchronous)

What about the second way of adding business logic? The approach is different, and it is described in 3factor.app architecture proposal. The idea of 3factor.app architecture is having async serverless, reliable eventing, and Realtime GraphQL (subscriptions and live queries) as 3 essential factors of modern software architecture.

In fact, at React Europe, I gave a talk which was called "Redux style backends."

The main concept was to bring Redux mental model to software architecture. It looks like this:

The idea behind this is that Database is your state, Eventing system purpose is to trigger events(actions) which will call serverless functions(reducers) to execute custom logic. Serverless functions, in turn, will call mutations on Hasura GraphQL Endpoint changing state to a new one. In the end, because of real-time GraphQL, everything will be propagated to the client through subscriptions and live queries.

1. Serverless functions

To add serverless function as an event trigger, you need to go to the Event trigger tab and add it there. You will have a bunch of options that you can include such as trigger events only on specific column updates and so on

If you are interested in checking a live example, I've live-streamed serverless function with Netlify some time ago on our Hasura Twitch stream, so you can check it here:

2. Using the eventing system to trigger webhooks

Serverless functions attached to event triggers are not the only thing you can do with events. Basically any url can be attached to event and it's up to you what you do inside. Let's take a look at example from [Hasura learn tutorial] of creating an event trigger for sending email:

const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport('smtp://'+process.env.SMTP_LOGIN+':'+process.env.SMTP_PASSWORD+'@' + process.env.SMTP_HOST);
const fs = require('fs');
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.set('port', (process.env.PORT || 3000));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(function(req, res, next) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Cache-Control', 'no-cache');
    next();
});
app.post('/send-email', function(req, res) {

  const name = req.body.event.data.new.name;
  // setup e-mail data
  const mailOptions = {
      from: process.env.SENDER_ADDRESS, // sender address
      to: process.env.RECEIVER_ADDRESS, // list of receivers
      subject: 'A new user has registered', // Subject line
      text: 'Hi, This is to notify that a new user has registered under the name of ' + name, // plaintext body
      html: '<p>'+'Hi, This is to notify that a new user has registered under the name of ' + name + '</p>' // html body
  };
  // send mail with defined transport object
  transporter.sendMail(mailOptions, function(error, info){
      if(error){
          return console.log(error);
      }
      console.log('Message sent: ' + info.response);
      res.json({'success': true});
  });

});
app.listen(app.get('port'), function() {
  console.log('Server started on: ' + app.get('port'));
});

Enter fullscreen mode Exit fullscreen mode

As you can see, it's a straightforward express server that sends mail based on event payload, it receives.

Summary

As you can see there are lots of options to add your business logic to Hasura, but if you have doubt and not sure how to integrate Hasura with your existing architecture, contact us on Discord, and we will figure out how to help.

Top comments (5)

Collapse
 
birdinadream profile image
Les

Is there a big disadvantage to schema stitching Hasura into your (Apollo) server instead of the other way around?

Collapse
 
mschettinoo profile image
Matheus Schettino

Curious about that too.

Collapse
 
birdinadream profile image
Les

I spoke to the guys from Hasura at graphqlconf. So theoretically there is no downside, but Hasura has a lot of optimizations to make it super fast.
So unless your own server implementation is perfect the chance it becomes the bottleneck is a bit higher than when you do it the other way around.

Hasura is also launching remote relations, which should make adding in logic easier

Some comments may only be visible to logged-in visitors. Sign in to view all comments.