Build a Graphql Api for Node & MYSQL 2019— JWT

Brian Schardt
9 min readNov 7, 2018

--

If you are here you probably already know. You know that Graphql is FREAKING awesome, speeds up development, and is probably the best thing that has happened since Tesla released the model S.

Here is a new template that I use: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

However, most tutorials I have read show how to build a graphql app but introduce the common n+1 requests problem. As a result, performance is usually super poor.

Really is this better than a Tesla?

My goal in this article is not to explain the basics of Graphql, but to show someone how to quickly build a Graphql API that does not have the n + 1 issue.

If you want to know why 90% of new applications should use graphql api’s instead of restful click here.

Video Supplement:

This template IS MEANT to be used for production as it contains easy ways to manage environment variables, and has an organized structure so code will not get out of hand. To manage the n + 1 issue, we use data loading, the thing facebook released to solve this issue.

Authentication : JWT

ORM: Sequelize

Database: Mysql, or Postgres

Other Important packages used: express, apollo-server, graphql-sequelize, dataloader-sequelize

Note: Typescript is used for the app. It is so similar to javascript, if you have never used typescript I would not worry. However, if there is enough demand I will write a regular javascript version. Comment if you would like that.

Getting Started

Clone the repo and install node modules

Here is a link to the repo, I recommend cloning it to best follow along.

git clone git@github.com:brianschardt/node_graphql_apollo_template.git
cd node_graphql_apollo_template
npm install
//install global packages to run application
npm i -g nodemon

Lets start with .env

Rename example.env to .env and change it to the correct credentials for your environment.

NODE_ENV=development

PORT=3001

DB_HOST=localhost
DB_PORT=3306
DB_NAME=type
DB_USER=root
DB_PASSWORD=root
DB_DIALECT=mysql

JWT_ENCRYPTION=randomEncryptionKey
JWT_EXPIRATION=1y

Run the code

Now if you database is running and you have correctly updated your .env file with the proper information we should be able to run our app. This will create the tables with the defined schema automatically in the database.

//use for development as this watches changes in the code.
npm run start:watch
//use for production
npm run start

Now goto your browser and enter: http://localhost:3001/graphql

You should see now graphql playground which allows you to view documentation on what mutations, and queries already exist. It also allows you to make queries against the API. There are a couple of them already made, but to fully test out the power of this template API you may want to manually seed the database with information.

Database and Graphql Schema

As you see from looking at the schema’s on graphql playground it has a pretty simple structure. There are only 2 tables, i.e. User, and Company. A User can belong to one Company and a Company can have many users, i.e. a one to many association.

Create a User

Example gql to run in the playground to create a user. This will also return a JWT so you can authenticate for future requests.

mutation{
createUser(data:{firstName:"test", email:"test@test.com", password: "1"}){
id
firstName
jwt
}
}

Authenticate:

Now that you have the JWT, lets test authentication with gql playground to make sure everything is working correctly. On the left bottom of the webpage there will be text that says HTTP HEADERS. Click on it and enter this:

Note: replace with your token.

{
"Authorization": "Bearer eyJhbGciOiJ..."
}

Now run this query in the playground:

query{
getUser{
id
firstName
}
}

If everything worked your name and user id should be returned.

Now if you manually seed your database, with a company name and id, and assign that id to your user, and run this query. The company should be returned.

query{
getUser{
id
firstName
company{
id
name
}
}
}

Ok now that you know how to use and test this API lets get into the code!

Code Dive

Main file — app.ts

Load Dependencies — loads db models, and env variables.

import * as express  from 'express';
import * as jwt from 'express-jwt';
import { ApolloServer } from 'apollo-server-express';
import { sequelize } from './models';
import { ENV } from './config';

import { resolver as resolvers, schema, schemaDirectives } from './graphql';
import { createContext, EXPECTED_OPTIONS_KEY } from 'dataloader-sequelize';
import to from 'await-to-js';

const app = express();

Setup middleware and Apollo Server!

Note: the “createContext(sequelize)” is what gets rid of the n+1 problem. This is all done in the background by sequelize now. MAGIC!! This uses the facebook dataloader package.

const authMiddleware = jwt({
secret: ENV.JWT_ENCRYPTION,
credentialsRequired: false,
});
app.use(authMiddleware);
app.use(function (err, req, res, next) {
const errorObject = {error: true, message: `${err.name}:
${err.message}`};
if (err.name === 'UnauthorizedError') {
return res.status(401).json(errorObject);
} else {
return res.status(400).json(errorObject);
}
});
const server = new ApolloServer({
typeDefs: schema,
resolvers,
schemaDirectives,
playground: true,
context: ({ req }) => {
return {
[EXPECTED_OPTIONS_KEY]: createContext(sequelize),
user: req.user,
}
}
});
server.applyMiddleware({ app });

Listen for requests

app.listen({ port: ENV.PORT }, async () => {
console.log(`🚀 Server ready at http://localhost:${ENV.PORT}${server.graphqlPath}`);
let err;
[err] = await to(sequelize.sync(
// {force: true},
));

if(err){
console.error('Error: Cannot connect to database');
} else {
console.log('Connected to database');
}
});

Configuration variables — config/env.config.ts

We use the dotenv to load in our .env variables to our app.

import * as dotEnv from 'dotenv';
dotEnv.config();

export const ENV = {
PORT: process.env.PORT || '3000',

DB_HOST: process.env.DB_HOST || '127.0.0.1',
DB_PORT: process.env.DB_PORT || '3306',
DB_NAME: process.env.DB_NAME || 'dbName',
DB_USER: process.env.DB_USER || 'root',
DB_PASSWORD: process.env.DB_PASSWORD || 'root',
DB_DIALECT: process.env.DB_DIALECT || 'mysql',

JWT_ENCRYPTION: process.env.JWT_ENCRYPTION || 'secureKey',
JWT_EXPIRATION: process.env.JWT_EXPIRATION || '1y',
};

Graphql time!!!

Let’s take a look at those resolvers!

graphql/index.ts

Here we are using the package schema glue. This helps break up our schema, queries, and mutations into seperate parts to maintain clean and organized code. This package automatically searches the directory we specify for 2 files, i.e. schema.graphql, and resolver.ts. It then grabs them and glues them together. Hence the name schema glue.

Directives: for our directives we create a directory for them and include them via an index.ts file.

import * as glue from 'schemaglue';
export { schemaDirectives } from './directives';
export const { schema, resolver } = glue('src/graphql', { mode: 'ts' });

We are making directories for each model we have for consistency. Thus, we have the a user and company directory.

graphql/user

We noticed the resolver file, even when using schema glue, can still get very large. So we decided to break it up further based on if it is a query, mutation, or map for a type. Thus, we have 3 more files.

  • user.query.ts
  • user.mutation.ts
  • user.map.ts

Note: If you want to add gql subscriptions you would create another file called: user.subscription.ts and include it in the resolver file.

graphql/user/resolver.ts

This file is pretty simple and servers to organize the other files in this directory.

import { Query } from './user.query';
import { UserMap } from "./user.map";
import { Mutation } from "./user.mutation";

export const resolver = {
Query: Query,
User: UserMap,
Mutation: Mutation
};

graphql/user/schema.graphql

This file defines our graphql schema, and resolvers! Super important!

type User {
id: Int
email: String
firstName: String
lastName: String
company: Company
jwt: String @isAuthUser
}

input UserInput {
email: String
password: String
firstName: String
lastName: String
}

type Query {
getUser: User @isAuth
loginUser(email: String!, password: String!): User
}

type Mutation {
createUser(data: UserInput): User
}

graphql/user/user.query.ts

This file contains the functionality for all of our user queries and mutations. Uses the magic from graphql-sequelize to handle a lot of the graphql stuff. If you have used other graphql packages or tried creating your own graphql api, you will recognize how important and time saving this package is. Yet it still provides you all the customization you will ever need! Here’s a link to documentation on that package.

import { resolver } from 'graphql-sequelize';
import { User } from '../../models';
import to from 'await-to-js';

export const Query = {
getUser: resolver(User, {
before: async (findOptions, {}, {user}) => {
return findOptions.where = {id: user.id};
},
after: (user) => {
return user;
}
}),
loginUser: resolver(User, {
before: async (findOptions, { email }) => {
findOptions.where = {email};
},
after: async (user, { password }) => {
let err;
[err, user] = await to(user.comparePassword(password));
if(err) {
console.log(err);
throw new Error(err);
}

user.login = true;//to let the directive know to that this user is authenticated without an authorization header
return user;
}
}),
};

graphql/user/user.mutation.ts

This file contains all the mutation for the user section of our app.

import { resolver as rs } from 'graphql-sequelize';
import { User } from '../../models';
import to from 'await-to-js';

export const Mutation = {
createUser: rs(User, {
before: async (findOptions, { data }) => {
let err, user;
[err, user] = await to(User.create(data) );
if (err) {
throw err;
}
findOptions.where = { id:user.id };
return findOptions;
},
after: (user) => {
user.login = true;
return user;
}
}),
};

graphql/user/user.map.ts

This is the one that people always overlook, and makes coding and querying in graphql so difficult and have poor performance. However, all the packages we have included solves the issue. Mapping types to each other is what gives graphql its power and strength, but people code it in such a way that makes this strength turn into a weakness. However, all the packages we have used gets rid of that in an easy way.

import { resolver } from 'graphql-sequelize';
import { User } from '../../models';
import to from 'await-to-js';

export const UserMap = {
company: resolver(User.associations.company),
jwt: (user) => user.getJwt(),
};

Yes that is it so simple!!!

Note: the graphql directives in the user schema are what protect certain fields like the JWT field on user and the getUser query.

Models — models/index.ts

We use the sequelize typescript so we can set variables to this class type. In this file we start by loading the packages. Then we instantiate sequelize and connect it to our db. Then we export the models.

import { Sequelize } from 'sequelize-typescript';
import { ENV } from '../config/env.config';

export const sequelize = new Sequelize({
database: ENV.DB_NAME,
dialect: ENV.DB_DIALECT,
username: ENV.DB_USER,
password: ENV.DB_PASSWORD,
operatorsAliases: false,
logging: false,
storage: ':memory:',
modelPaths: [__dirname + '/*.model.ts'],
modelMatch: (filename, member) => {
return filename.substring(0, filename.indexOf('.model')) === member.toLowerCase();
},
});
export { User } from './user.model';
export { Company } from './company.model';

The modelPaths and modelMatch are extra options that tell sequelize-typescript where our models are and what their naming conventions are.

Company Model — models/company.model.ts

Here we define the company schema using sequelize typescript.

import { Table, Column, Model, HasMany,  PrimaryKey, AutoIncrement } from 'sequelize-typescript';
import { User } from './user.model'
@Table({timestamps: true})
export class Company extends Model<Company> {

@Column({primaryKey: true, autoIncrement: true})
id: number;

@Column
name: string;

@HasMany(() => User)
users: User[];
}

User Model — models/user.model.ts

Here we define the user model. We will also add some custom functionality for authentication.

import { Table, Column, Model, HasMany,  PrimaryKey, AutoIncrement, BelongsTo, ForeignKey, BeforeSave } from 'sequelize-typescript';
import { Company } from "./company.model";
import * as bcrypt from 'bcrypt';
import to from 'await-to-js';
import * as jsonwebtoken from'jsonwebtoken';
import { ENV } from '../config';

@Table({timestamps: true})
export class User extends Model<User> {
@Column({primaryKey: true, autoIncrement: true})
id: number;

@Column
firstName: string;

@Column
lastName: string;

@Column
email: string;

@Column
password: string;

@ForeignKey(() => Company)
@Column
companyId: number;

@BelongsTo(() => Company)
company: Company;
jwt: string;
login: boolean;
@BeforeSave
static async hashPassword(user: User) {
let err;
if (user.changed('password')){
let salt, hash;
[err, salt] = await to(bcrypt.genSalt(10));
if(err) {
throw err;
}

[err, hash] = await to(bcrypt.hash(user.password, salt));
if(err) {
throw err;
}
user.password = hash;
}
}

async comparePassword(pw) {
let err, pass;
if(!this.password) {
throw new Error('Does not have password');
}

[err, pass] = await to(bcrypt.compare(pw, this.password));
if(err) {
throw err;
}

if(!pass) {
throw 'Invalid password';
}

return this;
};

getJwt(){
return 'Bearer ' + jsonwebtoken.sign({
id: this.id,
}, ENV.JWT_ENCRYPTION, { expiresIn: ENV.JWT_EXPIRATION });
}
}

That is a lot of code right there, so comment if you want me to break it down.

If you have any suggestions for improvements please let me know! If you want me to make a template in regular javascript also let me know! Also, If you have any questions I will try to respond same day so please don’t be scared to ask!

Thanks,

Brian Schardt

--

--