Vincent A Saulys' Blog
Anatomy of Express, Sequelize, & GraphQL
Tags: javascript
July 01, 2021

GraphQL has gotten really popular with frontend developers lately. It helps to simplify what interface they query against while giving them control over what's returned.

As powerful as this is, there's not a lot written about how to integrate it into projects. Many, projects wave their hands in a pot of syntactic sugar where too much is done for you and its not obvious how it works in your specific case. Others assume no familiary with NodeJS leaving you to slog with knowledge you already know.

This guide is written to explain how to use GraphQL with ExpressJS, the most popular web framework for NodeJS, and Sequelize, the most popular ORM for NodeJS. It assumes the reader has some knowledge on how ExpressJS already works.

Why has GraphQL gotten popular? Why should I use it?

GraphQL is part of a trend with web applications where more and more work is done on the client-side in the browser instead of on servers in the backend.

This trend started with GMail in the 2000s. Before then, web applications were html page replies from servers to the browser. This is a very slow way to operate and feels like flipping through pages in a magazine.

GMail solved this by integrating JavaScript to asynchonrously fetch content for displaying on the page. This felt closer to a native app on the desktop; more work was done locally with fewer page refreshes.

Then in the 2010s, innovations like Angular and BackboneJS came out. These frameworks simplified building GMail-like applications in what are now known as Single Page Applications (SPAs).

SPAs have improved user "delight" but have also bifurcated development. Frontend developers now need to talk to API endpoints deployed by backend developers. These endpoints are subject to change and can multiply quickly as projects scale.

GraphQL solves this issue by having one endpoint and one unified language for querying against. This vastly simplifies the cognitive overhead for frontend developers while keeping things stable for backend developers.

What does a GraphQL backend look like?

GraphQL development involves three pieces:

  1. model
  2. schema
  3. resolvers

For the most part, you build them in the order stated though your specific use case may differ. First you figure out what the data model will look like -- sequelize will be used in this guide -- followed by what the API endpoint will look like via the schema. Then you write the logic to tie the two together with the resolver.

GraphQL has a "standard" implementation in the form of express-graphql. This library works with expressJS and NodeJS to create a fully compliant GraphQL endpoint. With a little work, we can make it work with sequelize.

How do I build an express-graphql API?

The one-file-skeleton of an ExpressJS-GraphQL project looks as follows.

const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");

const schema = buildSchema(``);

const root = {};

const app = express();

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true  // display console in browser
  })
);

app.listen(3001, () => console.log("running graphql"));

We define the models with sequelize (elsewhere), import them into our app, describe what's accessible to our end user in the schema, then write how that schema gets resolved in the root.

Remember our workflow: models first. We'll need to create our models in sequelize. This guide will use sequelize-cli to do this.

We'll create a basic no-frills user object (emphasis on the no-frills!):

npx sequelize-cli model:generate --name User \
  --attributes name:string,email:string,state:boolean,birth:date,thing:integer

This will create a model like below:

// ./models/user.js
'use strict'; 
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class User extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
    }
  };
  User.init({
    name: DataTypes.STRING,
    email: DataTypes.STRING,
    state: DataTypes.BOOLEAN,
    birth: DataTypes.DATE,
    thing: DataTypes.INTEGER
  }, {
    sequelize,
    modelName: 'User',
  });
  return User;
};

We can now import this model into our skeleton like above:

const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");

const { User } = require("./models");

// ... excised

The database will need to be migrated as well. This guide won't cover that portion, but you can use MySQL via a docker container pretty easily. I have a preference for PostgreSQL myself.

How do we write our Schema?

GraphQL has a large schema language for defining what can be queried (think GET requests in REST terms) vs mutated (think POST request). This guide will not cover all of that here. Instead, we'll cover the very basics. The point of this writing is to establish the eb-and-flow of working with GraphQL.

Schemas define their types. Query and Mutation are reserved words in this instance. So a skeletal graphql schema will look like below:

type Query {
}

type Mutation {
}

type User {
}

Everything in query and mutation is expected to have a resolver.

Let's define a hello world endpoint with "hello":

const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");

const { User } = require("./models");

const schema = buildSchema(`
  type Query {
    hello: String
  }
`);

const root = {
  hello: () => {
    return "Hello World!";
  },
};

const app = express();

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true
  })
);

app.listen(3001, () => console.log("running graphql"));

Because we set graphiql to true, we can open the browser to http://localhost:3001/graphql and type in queries to test this. If we type:

{
  hello
}

We should get back "hello world"

image of working helle world

This is a trivial example though. What we want is to use our sequelize models.

Each endpoint here relies on a resolver indicated in root. We can add these models into the resolvers, which will accept async functions.

Before we write our resolver, we need to add the proper query to our schema. Before we can have the query in our schema, we need to add the type we're returning.

When defining our User type in the schema, fields can be ommitted if we don't want to return them. Sequelize includes createdAt and updatedAt fields automatically for instance. By not including them, they'll never be accessible to the frontend.

const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");

const { User } = require("./models");

const schema = buildSchema(`
  type Query {
    hello: String
    getAllUsers: [User]
  }

  type User {
    id: ID!
    name: String
    email: String
    state: Boolean
    thing: Int
  }
`);

const root = {
  hello: () => {
    return "Hello World!";
  },
  getAllUsers: async () => {
    return await User.findAll();
  }
};

const app = express();

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true
  })
);

app.listen(3001, () => console.log("running graphql"));

This endpoint will work but our database is not currently populated so nothing will be returned. We could use sequelize's seeding to do this but instead let's write a mutation.

Mutations are the equivalent to POST requests in GraphQL. They allow you to modify or create new entries by accepting values.

Again remembering the workflow of "schema then resolver", we'll type out the mutation line in camelcase:

  type Mutation {
    createUser(name: String!, email: String!): User
  }

To create a new user, we'll accept a name and email. They'll be required -- otherwise we'll get an error from the server.

Next we'll write the resolver. Arguments passed in the GraphQL schema appear in args when passed to the resolver. It's typically a good idea to destructuring them out of the object for readabilty. Out of a personal convention, underlines are used to mark objects local to a scope to prevent confusion with the class name.

const root = {
  // ... excised

  createUser: async (args) => {
    const { name, email } = args;
    const _user = await User.create({ name, email });
    return _user;
  }
};

Simple enough! If we run a query, we can create a user

Example of Mutation in GraphQL

And then we can query it out:

Example of getAllUsers in
GraphQL

This use of external arguments can be expanded for getting just one user too. The process will be as before: write the schema specifying the arguments then write the resolver

When done, the whole thing looks like this:

const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");

const { User } = require("./models");

const schema = buildSchema(`
  type Query {
    hello: String
    getAllUsers: [User]
    getUser(id: Int!): User 
  }

  type Mutation {
    createUser(name: String!, email: String!): User
  }

  type User {
    id: ID!
    name: String
    email: String
    state: Boolean
    thing: Int
  }
`);

const root = {
  hello: () => {
    return "Hello World!";
  },
  getAllUsers: async () => {
    return await User.findAll();
  },
  getUser: async (args) => {
    return await User.findByPk(args.id);
  },
  createUser: async (args) => {
    const { name, email } = args;
    const _user = await User.create({ name, email });
    return _user;
  }
};

const app = express();

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true
  })
);

app.listen(3001, () => console.log("running graphql"));

How do I build in logic for these resolvers?

The example above is very simple: we're getting and creating objects. There's nothing complicated at play.

In most cases, you'll want to do something with your logic beyond shoving it somewhere. That means putting in business logic.

There's thoughts on how to do this. Below are some that can be done when working with GraphQL.

Resolvers can carry the extra business logic.

This has a key advantage of not hiding any "side effects." You can see, in one place, what happens when an endpoint is used. That makes for very readable code

The other approach is to use hooks in your sequelize models. This keeps the logic close to where its operated (e.g. with your data representations) and may be beneficial if you want to keep encapsulated logic with a model (e.g. all side effects happen with that one model, everytime).

Hooks have a large write-up in the Sequelize docs. Below is how you could extend sequelize-cli generated models with hooks:

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class User extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
    }
  };
  User.init({
    name: DataTypes.STRING,
    email: DataTypes.STRING,
    state: DataTypes.BOOLEAN,
    birth: DataTypes.DATE,
    thing: DataTypes.INTEGER
  }, {
    sequelize,
    modelName: 'User',
    hooks: {
      beforeValidate: async (user, options) => {},
      afterValidate: async (user, options) => {},
      beforeCreate: async (user, options) => {}
      // etc. etc. 
    }
  });
  return User;
};
Share on...