A Comprehensive, Step-by-Step Guide to Building Highly Optimized GraphQL Servers

In web development, efficiently managing data requests and minimizing the pitfalls of underfetching and overfetching are pivotal for creating responsive and resource-efficient applications. GraphQL emerges as a powerful query language that addresses these issues by allowing clients to request exactly the data they need and nothing more. However, the elegance of a GraphQL solution hinges on its implementation.

This is where Ballerina, a cloud-native programming language, emerges. Designed for networked applications, It simplifies the task of building robust and scalable services, including GraphQL servers.

This article is a comprehensive step-by-step guide to crafting a GraphQL server with Ballerina. Our focus will be on running the server and optimizing data responses and the data load from various data sources. Whether working with a database, an API, or any data-providing service, GraphQL can integrate smoothly, retrieving exactly what is needed without any unnecessary data.

Photo by Joshua Mayo on Unsplash

Example Scenario: Banking Application

The data source to the GraphQL server can be anything, such as a database, API, or service that holds data. Also, GraphQL can interact with any combination of data sources. Let’s see how we can implement a GraphQL server using the Ballerina language to expose data in the MySQL database in this use case.

The MySQL database holds data about a hypothetical bank, and it has account details and employee details in two tables. Each account has an attached bank employee. So, the purpose of this application is to retrieve various data, such as

details of all accountsdetails of a specific accountaccounts under each bank employeeBanking Application Overview

Step 1: Get the Pre-requisites

Let’s start by installing Ballerina and the VSCode with Ballerina Plugin.

Install BallerinaInstall VSCode and Ballerina VSCode Plugin

Now, let’s get the sample MySQL database up.

If you are using a MySQL DB instance, create the sample MySQL database and populate it with data using the init.sql script.

mysql -u root -p < /path/to/db.sql

If you do not have MySQL setup, use Docker. First, install Docker. Alternatively, install Rancher Desktop, which includes Docker and Kubernetes. Then, download docker-compose.yml and run the following command from the location of the file.

docker-compose up

Step 2: Create a Ballerina Package

Ballerina organizes code using packages. Start by creating a Ballerina package, then generate the service code within this package, and proceed to implement the business logic.

Issue the following command in a terminal to create the package named bank-service. You can use a different package name, but adhere to the naming best practices.

bal new bank-service

You will be able to see the following files within the newly created package folder.

Step 3: Write the GraphQL service to return some dummy data

Let’s write the service code and return some dummy data. Some details of the service are as follows.

GraphQL service is running on port 9090 with the base path as bank.It will have a resolver function named accounts, which returns Account data.Ballerina GraphQL resolvers can return record values. The record will be mapped to a GraphQL output object type in the generated GraphQL schema with the same name and fields.Two Ballerina records named Account and BankEmployee are created to represent the data. Learn more about Ballerina Records in examples.

Note: Ballerina provides first-class support for services with intuitive syntax. A service represents a collection of remotely accessible methods attached to a particular listener.

Add the following code to the main.bal file.

import ballerina/graphql;

service /bank on new graphql:Listener(9090) {

resource function get accounts() returns Account[]|error {
return [
{
number: “123”,
accType: “Savings”,
holder: “John Doe”,
address: “20A, FM Road, Austin, TX, 73301”,
openedDate: “2020-01-01”,
bankEmployee: {id: 1, name: “Mary Jones”, position: “Operations Manager”}
}
];
}
}

type BankEmployee record {|
int id;
string name;
string position;
|};

type Account record {|
string number;
string accType;
string holder;
string address;
string openedDate;
BankEmployee bankEmployee;
|};

Run the service using the following command within the package root folder.

bal run

Issue the following GraphQL query, and you will get the hard-coded account details in the response.

Note: Refer to the Test the Service section for more details on the Try It tools.

Sample Request

GraphQL Query:

query {
accounts {
number
accType
holder
address
openedDate
bankEmployee {
id
name
position
}
}
}

Output:

{
“data”: {
“accounts”: [
{
“number”: “123”,
“accType”: “Savings”,
“holder”: “John Doe”,
“address”: “20A, FM Road, Austin, TX, 73301”,
“openedDate”: “2020-01-01”,
“bankEmployee”: {
“id”: 1,
“name”: “Mary Jones”,
“position”: “Operations Manager”
}
}
] }
}

Equivalent HTTP POST request:

curl -X POST -H “Content-type: application/json” -d “{“query”: “{ accounts { number accType holder address openedDate bankEmployee { id name position }}}”}” “http://localhost:9090/bank”

Step 4: Connect with the MySQL database to fetch data

Now, instead of sending hardcoded data, let’s connect to the sample database we populated already and query the data. For that, we can use Ballerina’s database connectors. Here are some details about the improved code.

MySQL client connector is added to fetch the data from DB, and instead of hard-coded values, the data is fetched using SQL SELECTquery.An additional record named DBAccount is defined to process the data loaded from DB.A transform function is added to convert the DBAccount record type values to Account type values.The graphql:ServiceConfig annotation is added to enable GraphiQL client capabilities to test the service.

Update the main.bal file with the following source code.

import ballerina/graphql;
import ballerina/sql;
import ballerinax/mysql;
import ballerinax/mysql.driver as _;

configurable string host = ?;
configurable string username = ?;
configurable string password = ?;
configurable string databaseName = ?;
configurable int port = ?;

final mysql:Client db = check new (host, username, password, databaseName, port);

@graphql:ServiceConfig {
graphiql: {
enabled: true
}
}
service /bank on new graphql:Listener(9090) {
resource function get accounts() returns Account[]|error {
return queryAccountData();
}
}

type BankEmployee record {
int id;
string name;
string position;
};

type Account record {
int number;
string accType;
string holder;
string address;
string openedDate;
BankEmployee bankEmployee;
};

type DBAccount record {|
int acc_number;
string account_type;
string account_holder;
string address;
string opened_date;
int employee_id;
string position;
string name;
|};

function queryAccountData() returns Account[]|error {
stream<DBAccount, sql:Error?> accountStream = db->query(`SELECT a.acc_number, a.account_type, a.account_holder, a.address,
a.opened_date, e.employee_id, e.position, e.name from Accounts a LEFT JOIN Employees e on a.employee_id = e.employee_id; `);

DBAccount[] dbAccounts = check from DBAccount dbAccount in accountStream
select dbAccount;
return transform(dbAccounts);
}

function transform(DBAccount[] dbAccount) returns Account[] => from var dbAccountItem in dbAccount
select {
number: dbAccountItem.acc_number,
accType: dbAccountItem.account_type,
holder: dbAccountItem.account_holder,
address: dbAccountItem.address,
openedDate: dbAccountItem.opened_date,
bankEmployee: {
id: dbAccountItem.employee_id,
name: dbAccountItem.name,
position: dbAccountItem.position
}
};

The database configurations are added as configurable variables in the above source code. So, you need to pass those values before running the service. There are many ways to provide values to configuration variables, and refer to the Provide values to configurable variables guide for more details.

For this example, let’s provide values via Config.toml file. Add a file named Config.tomlto your package root directory with the following content. If you are using a different DB, add the correct details here.

[graphql_bank]host= “localhost”
username= “root”
password= “root”
databaseName=”gq_bank_test”
port=3307

Then, rerun the service and issue the request using the same GraphQL query as in the previous step. Instead of the hardcoded single account value, you can see multiple account data fetched from the database.

Step 5: Add filtering with input parameters

Let’s say we need to filter the response based on the account number or the employee ID assigned to the accounts. We can do that by adding input parameters to the service resource function.

resource function get accounts(int? accNumber, int? employeeID) returns Account[]|error {

}

Then, we can use those parameters to generate a dynamic SQL query to filter out the data based on the where clause conditions of the query. Note that the input parameters are defined as optional parameters, so the API will work with or without them.

See the modified service code and query with input parameters below.

service /bank on new graphql:Listener(9090) {
resource function get accounts(int? accNumber, int? employeeID) returns Account[]|error {
return queryAccountData(accNumber, employeeID);
}
}

function queryAccountData(int? accNumber, int? employeeID) returns Account[]|error {
sql:ParameterizedQuery selectQuery = `SELECT a.acc_number, a.account_type, a.account_holder, a.address,
a.opened_date, e.employee_id, e.position, e.name from Accounts a LEFT JOIN Employees e on a.employee_id = e.employee_id `;

//Where clause
if accNumber != () || employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `WHERE `);
if accNumber != () {
selectQuery = sql:queryConcat(selectQuery, `a.acc_number = ${accNumber} `);
if employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `AND `);
}
}
if employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `e.employee_id = ${employeeID}`);
}
}
stream<DBAccount, sql:Error?> accountStream = db->query(selectQuery);

DBAccount[] dbAccounts = check from DBAccount dbAccount in accountStream
select dbAccount;
return transform(dbAccounts);
}

Some sample requests to test the above service are as follows.

Sample Request 1: Query with account number filtering

GraphQL Query:

query {
accounts(accNumber: 2) {
number
accType
holder
address
openedDate
bankEmployee {
id
name
position
}
}
}

Output:

{
“data”: {
“accounts”: [
{
“number”: 2,
“accType”: “FD”,
“holder”: “Emily Johnson”,
“address”: “234 Maple Lane Suburbia, FL 13579”,
“openedDate”: “2023-08-16”,
“bankEmployee”: {
“id”: 1,
“name”: “Mary Jones”,
“position”: “Operations Manager”
}
}
] }
}

Equivalent HTTP POST request:

curl -X POST -H “Content-type: application/json” -d “{“query”: “{ accounts(accNumber:2) { number accType holder address openedDate bankEmployee { id name position }}}”}” “http://localhost:9090/bank”

Sample Request 2: Query with employee id filtering

GraphQL query:

query {
accounts(employeeID: 1) {
number
accType
holder
address
openedDate
bankEmployee {
id
name
position
}
}
}

Output:

{
“data”: {
“accounts”: [
{
“number”: 1,
“accType”: “Savings”,
“holder”: “Robert Wilson”,
“address”: “101 Pine Road, Villagetown, TX 24680”,
“openedDate”: “2022-10-01”,
“bankEmployee”: {
“id”: 1,
“name”: “Mary Jones”,
“position”: “Operations Manager”
}
},
{
“number”: 2,
“accType”: “FD”,
“holder”: “Emily Johnson”,
“address”: “234 Maple Lane Suburbia, FL 13579”,
“openedDate”: “2023-08-16”,
“bankEmployee”: {
“id”: 1,
“name”: “Mary Jones”,
“position”: “Operations Manager”
}
}
] }
}

Equivalent HTTP POST request:

curl -X POST -H “Content-type: application/json” -d “{“query”: “{ accounts(employeeID:1) { number accType holder address openedDate bankEmployee { id name position }}}”}” “http://localhost:9090/bank”

Step 6: Add field-based filtering capabilities

So far, we have used a record named Account to represent the output object. The practice is to use a record to represent a GraphQL output object type only when all fields of that object type do not have any input arguments or when the field resolution does not require any complex logic execution.

Now, let’s say we need to add another field to the response called IsLocal to check whether the given bank account holder resides in one of the given states. To do that, we need to add an input parameter to the field, and since we need to check the address, we need to add the relevant logic.

In Ballerina, both the record type and the service type can function as Object types in GraphQL. When using records, each field in the record is transformed into a field in the GraphQL object, with the record field type being converted into the corresponding GraphQL field type.

On the other hand, if you opt for a service type, each resource method within the service acts as a field in the object. These resource methods may include input parameters, which are then mapped to arguments in the associated field.

You can find the modified code below.

Note the newly added resource method, which takes an input parameter and has logic within it without returning the value directly.

resource function get isLocal(State state) returns boolean {
return self.address.includes(state);
}

Complete code:

import ballerina/graphql;
import ballerina/sql;
import ballerinax/mysql;
import ballerinax/mysql.driver as _;

configurable string host = ?;
configurable string username = ?;
configurable string password = ?;
configurable string databaseName = ?;
configurable int port = ?;

final mysql:Client db = check new (host, username, password, databaseName, port);

@graphql:ServiceConfig {
graphiql: {
enabled: true
}
}
service /bank on new graphql:Listener(9090) {
resource function get accounts(int? accNumber, int? employeeID) returns Account[]|error {
return queryAccountData(accNumber, employeeID);
}
}

type BankEmployee readonly & record {
int id;
string name;
string position;
};

service class Account {
private final int number;
private final string accType;
private final string holder;
private final string address;
private final string openedDate;
private final BankEmployee bankEmployee;

function init(int number, string accType, string holder, string address, string openedDate, BankEmployee bankEmployee) {
self.number = number;
self.accType = accType;
self.holder = holder;
self.address = address;
self.openedDate = openedDate;
self.bankEmployee = bankEmployee;
}

// Each resource method becomes a field of the `Account` type.
resource function get number() returns int {
return self.number;
}
resource function get accType() returns string {
return self.accType;
}
resource function get holder() returns string {
return self.holder;
}

resource function get address() returns string {
return self.address;
}

resource function get openedDate() returns string {
return self.openedDate;
}

resource function get bankEmployee() returns BankEmployee {
return self.bankEmployee;
}

resource function get isLocal(State state) returns boolean {
return self.address.includes(state);
}
}

enum State {
TX,
CA,
NY
}

type DBAccount record {|
int acc_number;
string account_type;
string account_holder;
string address;
string opened_date;
int employee_id;
string position;
string name;
|};

function queryAccountData(int? accNumber, int? employeeID) returns Account[]|error {
sql:ParameterizedQuery selectQuery = `SELECT a.acc_number, a.account_type, a.account_holder, a.address,
a.opened_date, e.employee_id, e.position, e.name from Accounts a LEFT JOIN Employees e on a.employee_id = e.employee_id `;

// Where clause
if accNumber != () || employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `WHERE `);
if accNumber != () {
selectQuery = sql:queryConcat(selectQuery, `a.acc_number = ${accNumber} `);
if employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `AND `);
}
}
if employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `e.employee_id = ${employeeID}`);
}
}

stream<DBAccount, sql:Error?> accountStream = db->query(selectQuery);

DBAccount[] dbAccounts = check from DBAccount dbAccount in accountStream
select dbAccount;
return transform(dbAccounts);
}

function transform(DBAccount[] dbAccount) returns Account[] => from var dbAccountItem in dbAccount
select new Account(
dbAccountItem.acc_number,
dbAccountItem.account_type,
dbAccountItem.account_holder,
dbAccountItem.address,
dbAccountItem.opened_date,
{
id: dbAccountItem.employee_id,
position: dbAccountItem.position,
name: dbAccountItem.name
}
);

Sample Request: Get the details of accounts with input arguments in the fields

GraphQL query:

query {
accounts(employeeID: 1) {
number
accType
holder
address
openedDate
isLocal(state: TX)
bankEmployee {
id
name
position
}
}
}

Output:

{
“data”: {
“accounts”: [
{
“number”: 1,
“accType”: “Savings”,
“holder”: “Robert Wilson”,
“address”: “101 Pine Road, Villagetown, TX 24680”,
“openedDate”: “2022-10-01”,
“isLocal”: true,
“bankEmployee”: {
“id”: 1,
“name”: “Mary Jones”,
“position”: “Operations Manager”
}
},
{
“number”: 2,
“accType”: “FD”,
“holder”: “Emily Johnson”,
“address”: “234 Maple Lane Suburbia, FL 13579”,
“openedDate”: “2023-08-16”,
“isLocal”: false,
“bankEmployee”: {
“id”: 1,
“name”: “Mary Jones”,
“position”: “Operations Manager”
}
}
] }
}

Equivalent HTTP POST request:

curl -X POST -H “Content-type: application/json” -d “{“query”: “{ accounts(employeeID:1) { number accType holder address isLocal(state:TX) openedDate bankEmployee { id name position }}}”}” “http://localhost:9090/bank”

Step 7: Optimize DB data loading based on requested fields

Now, our GraphQL service is working as expected. But let’s optimize the data retrieval from the DB by adjusting the dynamic SQL query further based on the fields requested in the GraphQL query. If only a few fields are requested from each table, loading all the column data is unnecessary. Also, if the employee details attached to each account are not requested in the GraphQL query, we can avoid joining that table.

You can get the details about fields in the incoming GraphQL query using graphql:Field. This represents the information about a particular field of a GraphQL document.

The final code after implementing the dynamic queries is given below.

import ballerina/graphql;
import ballerina/sql;
import ballerinax/mysql;
import ballerinax/mysql.driver as _;

configurable string host = ?;
configurable string username = ?;
configurable string password = ?;
configurable string databaseName = ?;
configurable int port = ?;

final mysql:Client db = check new (host, username, password, databaseName, port);

@graphql:ServiceConfig {
graphiql: {
enabled: true
}
}
service /bank on new graphql:Listener(9090) {
resource function get accounts(graphql:Field gqField, int? accNumber, int? employeeID)
returns Account[]|error {
return queryAccountData(gqField, accNumber, employeeID);
}
}

type BankEmployee readonly & record {|
int? id;
string? name;
string? position;
|};

service class Account {
private final int? number;
private final string? accType;
private final string? holder;
private final string? address;
private final string? openedDate;
private final BankEmployee bankEmployee;

function init(int? number, string? accType, string? holder, string? address, string? openedDate, BankEmployee bankEmployee) {
self.number = number;
self.accType = accType;
self.holder = holder;
self.address = address;
self.openedDate = openedDate;
self.bankEmployee = bankEmployee;
}

// Each resource method becomes a field of the `Account` type.
resource function get number() returns int? => self.number;

resource function get accType() returns string? => self.accType;

resource function get holder() returns string? => self.holder;

resource function get address() returns string? => self.address;

resource function get openedDate() returns string? => self.openedDate;

resource function get bankEmployee() returns BankEmployee => self.bankEmployee;

resource function get isLocal(State state) returns boolean? {
string? address = self.address;
if address is () {
return false;
}
return address.includes(state);
}
}

enum State {
TX,
CA,
NY
}

type DBAccount record {|
int acc_number?;
string account_type?;
string account_holder?;
string address?;
string opened_date?;
int employee_id?;
string position?;
string name?;
|};

function queryAccountData(graphql:Field gqField, int? accNumber, int? employeeID) returns Account[]|error {
graphql:Field[]? subFields = gqField.getSubfields();
if subFields is () {
return error(“Invalid query with no sub fields”);
}

//Select clause
boolean isJoin = false;
boolean addressAdded = false;
sql:ParameterizedQuery selectQuery = `SELECT `;
int i = 0;
foreach graphql:Field subField in subFields {
i = i + 1;
match subField.getName() {
“number” => {
selectQuery = sql:queryConcat(selectQuery, `a.acc_number`);
}
“accType” => {
selectQuery = sql:queryConcat(selectQuery, `a.account_type`);
}
“holder” => {
selectQuery = sql:queryConcat(selectQuery, `a.account_holder`);
}
“address”|”isLocal” => {
if addressAdded {
continue;
}
selectQuery = sql:queryConcat(selectQuery, `a.address`);
addressAdded = true;

}
“openedDate” => {
selectQuery = sql:queryConcat(selectQuery, `a.opened_date`);
}
“bankEmployee” => {
graphql:Field[]? employeeSubFields = subField.getSubfields();
if employeeSubFields is () {
return error(“Invalid query with no employee sub fields”);
}
isJoin = true;
int j = 0;
foreach graphql:Field empSubField in employeeSubFields {
match empSubField.getName() {
“id” => {
selectQuery = sql:queryConcat(selectQuery, `e.employee_id`);
}
“name” => {
selectQuery = sql:queryConcat(selectQuery, `e.name`);
}
“position” => {
selectQuery = sql:queryConcat(selectQuery, `e.position`);
}
}
j = j + 1;
if j < employeeSubFields.length() {
selectQuery = sql:queryConcat(selectQuery, `, `);
}
}
}
}

if i < subFields.length() {
selectQuery = sql:queryConcat(selectQuery, `, `);
}
}
//From Clause
selectQuery = sql:queryConcat(selectQuery, ` from Accounts as a `);
if isJoin {
selectQuery = sql:queryConcat(selectQuery, ` LEFT JOIN Employees as e on a.employee_id = e.employee_id `);
}
//Where clause
if accNumber !is () || employeeID !is () {
selectQuery = sql:queryConcat(selectQuery, `WHERE `);
if accNumber != () {
selectQuery = sql:queryConcat(selectQuery, `a.acc_number = ${accNumber} `);
if employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `AND `);
}
}
if employeeID != () {
selectQuery = sql:queryConcat(selectQuery, `e.employee_id = ${employeeID}`);
}
}

stream<DBAccount, sql:Error?> accountStream = db->query(selectQuery);
DBAccount[] dbAccounts = check from DBAccount dbAccount in accountStream
select dbAccount;
return transform(dbAccounts);
}

function transform(DBAccount[] dbAccount) returns Account[] => from var dbAccountItem in dbAccount
select new Account(
dbAccountItem?.acc_number,
dbAccountItem?.account_type,
dbAccountItem?.account_holder,
dbAccountItem?.address,
dbAccountItem?.opened_date,
{
id: dbAccountItem?.employee_id,
position: dbAccountItem?.position,
name: dbAccountItem?.name
}
);

Test the Service

We need to use a client to call the GraphQL server. There are a few options for doing that.

Use the Ballerina GraphQL Try it tool.Use the built-in GraphiQL client in Ballerina.With curl, send an HTTP POST request to the endpoint, passing the GraphQL query as the query field in a JSON payload.

For all the requests, the endpoint is http://localhost:9090/bank

Complete Source Code

Refer to the graphql GitHub repository to find the complete source code with the Ballerina project structure.

Wrapping Up

In this post, we explored how to develop a GraphQL service using Ballerina and then how to use the Ballerina GraphQL features to add various GrapQL query filtering capabilities. Also, we improved the GraphQL service to have more efficient data loading practices by generating dynamic SQL queries based on the input parameters and fields in the query.

If you would like to learn more about Ballerina GraphQL support, please refer to the following documentation.

Ballerina GraphQL service examples

Ballerina GraphQL client examples

Ballerina GraphQL library specification

Mastering GraphQL Server Implementation with Ballerina was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.