Create your first Serverless GraphQL API with AWS Lambda and MySQL Database
By: Shadi Ayoub
Tags: Lambda, MySQL, Aws Api Gateway, Serverless, GraphQL

Step 4: Create the Local MySQL Database and run the Application Locally

Before we proceed with creating our Amazon RDS resource (i.e. Amazon MySQL Instance) on AWS, we will test the application locally using a local MySQL database. This is possible by using the serverless-offline plugin we previously added to our project.

First, let’s create our local database. Install MySQL on your machine using the guidelines on the formal MySQL website. I am using the MySQL server installed as part of WAMP Windows web development environment. You can also do the same or go to other options like XAMPP or MAMP. Also, make sure to install MySQL Workbench as we will use it to access our database for the purpose of verification.

Open your MySQL Workbench and create a new MySQL connection using the simple steps used here and then create a local MySQL database named graphql_db_local1 (or you can choose any meaningful name) using the default connection. The default username in this case would be root with no password required to connect to the created database.

Not having a password in this case is okay since our database is local on our machine. However, for the AWS MySQL Instance, connecting to the database will be a different story as you will see in the next section.

We need to keep our configuration information, which includes passwords and keys, separate. Rather than having them hard-coded in the different code files, these secrets can be read from configurations files. This is a recommended good practice! So let’s implement it.

Create the Config folder in the project root directory. Create two configuration files inside the folder: config.dev.yml that will contain the AWS cloud configurations and config.offline.yml that will contain the local configurations to connect to the local database. We will keep the config.dev.yml empty for now.

If you are using Git, remember to exclude committing the Config folder to GitHub by adding the /Config line in the .gitignore file.

Add the configurations below in the config.offline.yml file:

MYSQL_DB_NAME: graphql_db_local1

MYSQL_DB_USERNAME: root

MYSQL_DB_PASSWORD:

MYSQL_DB_HOST: 127.0.0.1

MYSQL_DB_PORT: 3306

NODE_ENV: dev

REGION: us-east-1

Open your serverless.yml file and add a new section called custom just before the provider section and add the line that reads from the configuration file as follows:

custom:

#################################################################

## Cloud Deployment Config ##

#################################################################

## run sls deploy --stage prod to select the config.prod.yml

## run sls deploy --stage dev to select the config.dev.yml #secrets: ${file(./Config/config.${opt:stage,self:provider.stage, 'dev'}.yml)} #################################################################

## Local Deployment ##

#################################################################

secrets: ${file(./Config/config.offline.yml)}

In the above example, we are defining the secrets variable that represents the configuration object being read from the corresponding configuration file. Variables allow us to dynamically replace configuration values in serverless.yml . You can read more about variables here.

Later, when we deploy to AWS we will have the option to deploy either to the production or the development (default) environment. For now, we are commenting the line related to reading from cloud configuration file and use the secrets variable that reads from the local configuration file. I hope that the concept of isolating configurations we used here is clear to you.

Add the region and environment configurations inside the provider section in serverless.yml file as shown below:

region: ${self:custom.secrets.REGION} environment:

NODE_ENV: ${self:custom.secrets.NODE_ENV} #mysql

MYSQL_DB_NAME: ${self:custom.secrets.MYSQL_DB_NAME}

MYSQL_DB_USERNAME: ${self:custom.secrets.MYSQL_DB_USERNAME}

MYSQL_DB_PASSWORD: ${self:custom.secrets.MYSQL_DB_PASSWORD}

MYSQL_DB_HOST: ${self:custom.secrets.MYSQL_DB_HOST}

MYSQL_DB_PORT: ${self:custom.secrets.MYSQL_DB_PORT}

Basic knowledge in SQL syntax is required to understand the SQL queries used in the following examples. A good resource can be found here.

mysql_common.js

Now let’s add the required code for our GraphQL Resolvers. First add the code below in the /Resolvers/Common/mysql_common.js file:

exports.init = async (client) => {

await client.query(`

CREATE TABLE IF NOT EXISTS users

(

id INT UNSIGNED NOT NULL AUTO_INCREMENT,

created DATETIME DEFAULT CURRENT_TIMESTAMP,

uuid CHAR(36) NOT NULL,

name VARCHAR(100) NOT NULL,

PRIMARY KEY (id)

);

`); await client.query(`

CREATE TABLE IF NOT EXISTS posts

(

id INT UNSIGNED NOT NULL AUTO_INCREMENT,

created DATETIME DEFAULT CURRENT_TIMESTAMP,

uuid CHAR(36) NOT NULL,

text VARCHAR(100) NOT NULL,

user_id MEDIUMINT UNSIGNED NOT NULL,

PRIMARY KEY (id)

);

`);

}; exports.getUser = async (client, uuid) => {

var user = {};

var userFromDb = await client.query(

`

select id, uuid, name from users where uuid = ?

`,

[uuid]

);

if (userFromDb.length == 0) {

return null;

}

var postsFromDb = await client.query(

`

select uuid, text from posts where user_id = ?

`,

[userFromDb[0].id]

); user.UUID = userFromDb[0].uuid;

user.Name = userFromDb[0].name; if (postsFromDb.length > 0) {

user.Posts = postsFromDb.map(function (x) {

return { UUID: x.uuid, Text: x.text };

});

}

return user;

};

This file provides two “export” functions:

the init function simply creates our tables if they do no exist in the database. This will happen only in the first time we connect to our database. One can choose to create the tables manually on the database using, say, MySQL Workbench. However, having the init function will help us during the development phase as the database may be required to be initialized multiple of times, which would make it a convenient technique. the getUser function that will be used by both the Query and Mutation functions as will be seen later. This function accepts two arguments namely, client and uuid . The client argument holds the MySQL connection object needed to connect to our MySQL database. The uuid argument holds the UUID of the user we want to get from the database. The function returns the user object or null if the user does not exist. The user object has three fields namely, UUID , Name , and the Posts array.

Explanations specific to the Javascript language is left to the reader to understand. A basic tutorial about JavaScript is provided by W3Schools.com

mysql_getUser.js

Add the code below to the empty mysql_getUser.js file residing in the Queries sub-folder, which represents a GraphQL Query:

var common = require("../Common/mysql_common");

const Client = require("serverless-mysql");

exports.func = async (_, { uuid }) => {

var client = Client({

config: {

host: process.env.MYSQL_DB_HOST,

database: process.env.MYSQL_DB_NAME,

user: process.env.MYSQL_DB_USERNAME,

password: process.env.MYSQL_DB_PASSWORD,

},

});

await common.init(client);

var resp = await common.getUser(client, uuid);

client.quit();

return resp;

};

This file contains only one function that accepts the UUID as an argument, which is forwarded to the getUser function defined in the Common folder to get the user object. Prior to calling the getUser function, the MySQL connection object namely, client is created to be passed as a second argument with UUID . Also, the init common function is called prior to calling getUser to make sure that the tables are created in the database before reading from them. One can decide to skip calling the init function later once the application is deployed to the production and all tables are created in the Amazon MySQL Database.

It is important to close the database link using the serverless-mysql quit method after each SQL query.

mysql_createUser.js

Add the code below to the empty mysql_createUser.js file residing in the Mutations sub-folder, which represents a GraphQL Mutation:

const { v4: uuidv4 } = require("uuid");

var common = require("../Common/mysql");

const Client = require("serverless-mysql"); exports.func = async (_, obj) => {

var client = Client({

config: {

host: process.env.MYSQL_DB_HOST,

database: process.env.MYSQL_DB_NAME,

user: process.env.MYSQL_DB_USERNAME,

password: process.env.MYSQL_DB_PASSWORD,

},

}); await common.init(client);

var userUUID = uuidv4();

let user = await client.query("INSERT INTO users (uuid, name) VALUES(?,?)", [userUUID, obj.input.Name]);

for (let index = 0; index < obj.input.Posts.length; index++) {

const element = obj.input.Posts[index];

await client.query("INSERT INTO posts (uuid, text, user_id) VALUES(?, ?, ?)", [uuidv4(), element.Text, user.insertId]);

}

var resp = await common.getUser(client, userUUID);

client.quit();

return resp;

};

The file defines one function that can be called to create a new user in the database. The function returns the new user information as an object. This function should receive the name and the list of posts of the user from the GraphQL request.

Prior to inserting the information in the database, the function uses the uuid library to generate a unique identifier for the user as well as for each post object. Notice that the common getUser function is called to return the created user information object to the caller right after creating the user with the posts sent with the GraphQL HTTP request.

handler.js

Lastly, update the code in the handler.js file as per the below:

"use strict"; const { GraphQLServerLambda } = require("graphql-yoga");

var fs = require("fs"); const typeDefs = fs.readFileSync("./schema.gql").toString("utf-8"); const resolvers = {

Query: {

mysql_getUser: require("./resolver/Query/mysql_getUser").func,

},

Mutation: {

mysql_createUser: require("./resolver/Mutation/mysql_createUser").func,

},

}; const lambda = new GraphQLServerLambda({

typeDefs,

resolvers,

}); exports.server = lambda.graphqlHandler;

exports.playground = lambda.playgroundHandler;

As mentioned earlier, the handler.js file should contain your function code. The function definition in serverless.yml will point to the handler.js file and its “export” functions (the last two lines in the code above).

The handler file actually creates the GraphQL Server that will be used by our Lambda Function. It uses the defined Resolvers and Schema to instantiate the GraphQL Server for our application. In the above code, two functions are defined. The first one is the actual graphql handler or the GraphQL API itself, while the second function is the playground handler, that provides the GraphQL IDE that we will use to experiment and test our GraphQL API.

As you may noticed, two objects were used to create the GraphQL Server namely, the TypeDefs which represents the Schema itself and the resolvers . More parameters can be used such as Context and Middlewares but for the scope of this tutorial, we don’t need them.

Configuring the Serverless Functions & the API Gateway

At this point, our GraphQL API code is ready. However, we need to add the definitions of the two functions in our Serverless Framework Template. Also, we need to define the resource that routes HTTP requests to the corresponding function. Other than routing requests, the AWS API Gateway would triggers our functions in the event of receiving an HTTP request.

Update the serverless.yml file by adding the functions section using the code below:

functions:

graphql:

handler: handler.server

events:

- http:

path: /api

method: post

cors: true playground:

handler: handler.playground

events:

- http:

path: /api # a must!

method: get

cors: true

In the above code we are configuring two things. One, is the link to the GraphQL Server and Playground functions, which is straightforward. Second, is the HTTP endpoints, which is something I need to explain further.

AWS provides ways to define events that would trigger specific resources. One example is the event of receiving an HTTP request from a client. For this particular event, we need to create a web API with an HTTP endpoint for our Lambda Function by using Amazon API Gateway. This resource would accept the HTTP request and route it to the corresponding Lambda Function. For the purpose of this tutorial, we will not secure our API and keep it open to serve traffic over the internet since I assume that you will drop it after completing the tutorial. You can read more about the AWS API Gateway service by following this Link.

Serverless Framework provides an easy AWS API Gateway Events Syntax to create HTTP endpoints as Event sources for the AWS Lambda Functions (Check the events property in the above code). More details can be found on this page.

At this point, we are ready to deploy our API Function with all the resources needed so far.

Running the Application Locally

Now let’s see if all will work on our local machine. Remember to make sure that your MySQL server is running and you are reading the config.offline.yml file from your serverless.yml . Also, use the MySQL Workbench client to list your local database and notice that it does not initially contain any tables. Remember that the init common function we discussed before will create the two tables for us upon running the first GraphQL query since these tables do not initially exist.

Listing our Local Database before our first GraphQL query

Now let’s turn on our Serverless Offline Server by running the command below:

sls offline

Successfully running the offline Serverless Server

If everything go fine and no errors are identified by Serverless, you should receive the above output screen.

After running the command successfully in the Terminal, open your browser and enter the URL http://localhost:3000/dev/api in the address bar (this is an http get operation) to open the GraphQL Playground and start testing your GraphQL Mutation and Query resolvers.

Run the below Mutation to create the first user and verify that the new records are added in the two tables:

mutation {

mysql_createUser(

input: {

Name: "Shadi"

Posts: [

{ Text: "Lorem ipsum dolor sit amet." }

{ Text: "Proin consequat mauris." }

]

}

) {

Name

UUID

}

}

Running our Mutation locally in GraphQL Playground

Local Database view after running the GraphQL Mutation

Now, run the below Query to get the information of the newly created user. Use a separate Tab for this. The UUID here is copied from the response received after successfully running the above Mutation.

query {

mysql_getUser(uuid: "efcea128-aee3-4d6f-87fb-008a4fae4dcf") {

Name

UUID

}

}

Running our Query locally in GraphQL Playground

Now, after running our Lambda Function locally and verifying that all are working fine on our machine, it is time to deploy our API application on AWS.

Let’s move on!