Graphql In Action
Implementing Schema Resolvers
This is still a work in progress. New content is synced here as it gets ready.

Implementing schema resolvers

This chapter covers

  • Using Node.js drivers for PostgreSQL and MongoDB

  • Seeding local databases with test data

  • Using the GraphQL schema language to build a schema

  • Creating resolver functions to make a schema executable

  • Using an interface to communicate with a GraphQL service

  • Creating an executable schema object using constructor and type helper objects

  • Creating custom object types and handling errors

  • Working with asynchronous calls to the database service

In the previous chapter, we designed the database models for AZdev and came up with the initial structure of its GraphQL schema. In this chapter, we’ll use Node.js database drivers and the GraphQL.js implementation to expose the entities in the databases through the use of resolver functions. To make a GraphQL schema executable under a GraphQL API service, you need to implement resolver functions.

You need a modern version of Node.js installed in your OS to follow along from this point. If you don’t have Node or if you have an old version of it (anything less than 13.2), download the latest from nodejs.org and use that.

Some familiarity with the Node.js runtime is needed. You don’t need to be an expert in Node but if you have never worked with it before you should probably learn its basics before proceeding with this chapter. I wrote a short introductory book on Node which you can get at az.dev/node-intro.

Checkout az.dev/gia-updates to see the updates that you might need to work through all the code exampels of this book.

1. Node.js drivers for PostgreSQL and MongoDB

For the GraphQL runtime service to be able to communicate with databases like PostgreSQL and MongoDB, it needs a driver. We’ll use Node’s "pg" package and "mongodb" package for that purpose. These are not the only packages that can be used as drivers. They are however the most popular ones in the Node’s ecosystem.

Let’s start from complete scratch. Create a new directory to host the GraphQL API for AZdev.

$ mkdir azdev-api
$ cd azdev-api
All the commands in the rest of this chapter will be executed from inside this new azdev-api directory.

1.1. Installing dependencies

To make a directory into a Node project, we need to create a package.json file to host meta-information about the project and document its dependencies. We can use the npm init command to generate this file:

$ npm init -y
This command has an interactive mode if you run it without the -y flag. You can use that interactive mode to seed the file with different information than what the command can detect on its own. The -y flag is for "YES" and it’ll start the file with the default values that can be detected by the command.

Open up the generated package.json file in your editor and add a "type" property to the json content, give that property a value of "module":

  {
    "name": "azdev-api-app",
    "version": "1.0.0",
    "type": "module",

     ·-·-· 
  }

This step is needed because we will be using JavaScript ESM modules in this project and Node supports a different type of modules (named commonjs). We have to tell Node which type this project will be using.

With a package.json file in the root of this project, we can now document the project dependencies. We need 5 dependencies and we can get them all using the npm install command:

$ npm install express graphql express-graphql pg mongodb

The "express" package will help us run a web server to host the GraphQL service. The "graphql" package is for GraphQL.js, the JavaScript implementation of GraphQL. It’ll take care of things like validating GraphQL operations and executing them.

To work with a GraphQL runtime, we need an interface. This is where the "express-graphql" package comes in handy. It has an HTTP(s) listener function that can interface a GraphQL schema and is designed to use with a middleware-based Web framework like Express.

The "pg" and "mongodb" packages are the database drivers. They expose JavaScript APIs to execute operations for PostgreSQL and MongoDB. We’ll need to configure them to connect to these database services. Let’s start by testing these drivers on the local database schemas that we designed so far.

1.2. Environment variables

Create a directory under azdev-api to host the files of the project:

$ mkdir lib

PG_CONNECTION_STRING="postgres://USERNAME:PASSWORD:@localhost:5432/azdev"

If you use PostgreSQL.App on Mac, you can leave the PASSWORD part empty and use your macOS username in the USERNAME part

MDB_CONNECTION_STRING="mongodb://localhost:27017/azdev"

Under lib, create a file named config.js. This is the file that’ll define any configurable variables the API is going to use. You need to be able to run the GraphQL API service with different configurations in production environments. This is why the config.js will define its variables by reading from the process "environment". Here’s a starting point for config.js:

Listing 5. 1. lib/config.js | az.dev/gia
export const pgConfig = {
  database: process.env.PG_DATABASE,
};

export const mongoConfig = {
  database: process.env.MONGO_DATABASE,
  url: process.env.MONGO_URL,
};

When it’s time to run this service in production, we’ll define different process environment variables. For local development, create an .env file at the root of the project and seed it with the following lines:

Listing 5. 2. .env | az.dev/gia
export PG_DATABASE="YOUR_PG_DEV_DB_NAME_HERE"

export MONGO_DATABASE="azdev"
export MONGO_URL="mongodb://localhost:27017"

This .env file should be ignored by your source control tools. You’ll have a different .env file (or something similar) for your production environment.

To get the process environment variables defined, you can use the Linux source command:

$ source .env

You’ll need to do that before running the service. Don’t forget it!

In production (and some local environments), you’ll also have to specify a USER and PASSWORD to be able to connect to databases. Figure out if you need that in your local environment. For most local environments, leaving these out will get you connected to a local user that matches your operating system logged-in user.

1.3. Connecting To databases

Before a client can execute commands on a database and retrieve data out of it, it needs to "connect" to it. There are many ways to connect to both PostgreSQL and MongoDB from the Node drivers. We can do a one-time connection per sql statement in the pg driver but the better way is to have the driver manage a pool of open connections and reuse these connections as needed. Both drivers support this mode.

ch04 fig 05 gqlia
Figure 5. 1. Keeping pools of open connections to databases

Here are the files we need to manage a connections pool for both PostgreSQL and MongoDB:

Listing 5. 3. lib/db-clients/pg.js | az.dev/gia
import pg from 'pg';
import { pgConfig } from '../config.js';

export default async () => {
  const pgPool = new pg.Pool(pgConfig);

  // Test the connection
  const client = await pgPool.connect();
  const tableCountResp = await client.query(
    'select count(*) from information_schema.tables where table_schema = $1;',
    ['azdev']
  );
  client.release();

  console.log(
    'Connected to PostgreSQL | Tables count:',
    tableCountResp.rows[0].count
  );

  pgPool.on('error', (err) => {
    console.error('Unexpected PG client error', err);
    process.exit(-1);
  });

  return {
    pgPool,
    pgClose: async () => await pgPool.end(),
  };
};
Listing 5. 4. lib/db-clients/mongo.js | az.dev/gia
import mdb from 'mongodb';
import { mongoConfig } from '../config.js';

export default async () => {
  const client = new mdb.MongoClient(mongoConfig.url, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });
  try {
    await client.connect();
    const mdb = client.db(mongoConfig.database);

    // Test the connection
    const collections = await mdb.collections();
    console.log(
      'Connected to MongoDB | Collections count:',
      collections.length
    );

    return {
      mdb,
      mdbClose: () => client.close(),
    };
  } catch (err) {
    console.error('Error in MongoDB Client', err);
    process.exit(1);
  }
};

I like the style of including the .js extension in import statements for local ES modules but this is just a coding style preference. Node requires explicit extensions by default. If you want to import your ES modules without specifying extensions you can pass --es-module-specifier-resolution=node to the node command.

Drivers change their API often! Please check az.dev/gia to for the latest text of the above 2 files in case these API changed.

Put these new files under lib/db-clients. The next script is going to use them to connect to the databases and load some seed data in them.

The code in the above 2 files is just my preferred method of connecting to these databases. I make use of async/await for the MongoClient but you can also use a .then/.catch syntax or even use callbacks if you want.

Note how I made both files export an async function although that was not needed for the PostgreSQL pool. I also made both functions return an object with similar purposes. I find this kind of normalization helpful for entities in the project that are meant to do similar things.

Note also how I "tested" the connections to both databases by counting the numbers of tables/collections. This step is optional but it verifies that connections are successful when the server starts.

1.4. Importing dev data

Having test-data in all tables and collections is a great way to get us started and enable us to test the GraphQL queries before dealing with the GraphQL mutations! Make sure you get some realistic test data in all the database tables one way or another.

Don’t use short or random data when you’re seeding your test database. Try to have production-quality data even for testing. You’re not building an API to work with Foos or Bars. This is why the seed script I prepared features real Need entries with real content that I’ll be entering on the production site once its ready.

The tables and collections we’ve designed require valid data. You’ll have to insert a user record first and then use its generated id value to insert a Need record. You need to have an id value for an Approach before instructing MongoDB to add any dynamic data elements to it. This means we need a write operation followed by a read operation for each record we insert to PostgreSQL.

We can simplify the script we need here by picking our own "id" values instead of having to read them after each INSERT statement but PostgreSQL has a way to do both write and read operations in one statement! This is beyond the scope of this book but the seed script I’ve prepared makes use of this powerful technique.

This script and its associated data elements are too big to include in here. You can find them both at this book’s code helper link: az.dev/gia. Use this link to find the text for both seed-data/index.js and seed-data/data.js. Add these files to your project.

The seed-data script makes use of a feature in PostgreSQL that might not be enabled out of the box in your PostgreSQL service. If you get a crypto-related error, you need to enable the pgcrypto extension. You can do that in psql with this command:

Listing 5. 5. In psql
CREATE EXTENSION pgcrypto;

We will use this extension to manage the hashing of passwords and auth tokens in the users table.

Execute the following command to insert all dev data in PostgreSQL and MongoDB:

Listing 5. 6. Loading the seed data
$ npm run import-dev-data

You will need to run Node version 12.x or higher for the JavaScript ESM modules to work natively. If your version of Node is older than 13.2 then you’ll also need to add the --experimental-modules flag to the command line (that flag is no longer needed beginning with Node 13.2).

Alternatively, you can install the "esm" package from npm and alias the node command as node -r esm.

If everything runs successfully, you should have 5 Need entries with their Approaches and some extra dynamic data elements in MongoDB for each Approach. Use the following SQL queries to see the data in PostgreSQL:

Listing 5. 7. In "psql": queries to read data | az.dev/gia
SELECT * FROM azdev.users;

SELECT * FROM azdev.needs;

SELECT * FROM azdev.approaches;

For the data in MongoDB, you can use this find command to see it:

Listing 5. 8. In "mongo": command to read the approaches data | az.dev/gia
db.approachDetails.find({});

2. Setting up a GraphQL runtime

For an API server to work with the language used in GraphQL operations (like queries and mutations), it needs a runtime layer that can parse, validate, and execute these operations in addition to format and deliver the data back to the requesters.

Suppose we are creating a web application that needs to know the exact "current time" the server is using (and not rely on the client’s time). We’d like to be able to request the API server time with a query like this:

Listing 5. 9. Query the server for current time
{
  currentTime
}

To respond to this query, let’s make the server use an ISO UTC time string in the HH:MM:SS format:

Listing 5. 10. The format wanted for currentTime
{
  currentTime: "20:32:55"
}

This is a simple GraphQL request with a single operation (a query operation). GraphQL requests can also have multiple operations and include other information related to these operations (for example, variables).

For the server to accomplish the current time communication, it needs to:

  1. Have an interface for a requester to supply a GraphQL request.

  2. Parse the supplied request and make sure it has valid syntax according to the GraphQL language rules.

  3. Validate the request using a Schema. You can’t just run any request on a GraphQL server. You can only run the ones allowed by its schema. The server will also need to validate that all the required parts of the request are supplied. For example, if a query is using any variables then the server needs to validate their existence and make sure that they have the right types. If a request has more than one operation (for example, 2 or more queries), the server needs to validate that the request also includes the name of the operation that should be executed for the response.

  4. Resolve all fields in the request into scalar data elements. If the request is for a mutation operation, the server will need to perform the side effects of that mutation. If the request is for a subscription operation, the server will need to open a channel to communicate data changes when they happen

  5. Gather all the data for the response and serialize into a format like JSON. The serialized response needs to include the request structure and its resolved data (and any errors if the server encountered them).

  6. Have an interface for the requester to receive the response text generated for their request text.

All of these tasks are shared among all GraphQL requests the server has to deal with. In fact, except for the bolded words above (schema and resolve), all the mentioned tasks are shared among ALL GraphQL services. This means they can be abstracted and re-used. We don’t have to do them for each service.

Luckily, this has been done already! We don’t have to re-implement any of the steps above except for dealing with schema and resolvers. The rest is where a GraphQL "implementation" comes into the picture!

What exactly is a "GraphQL Implementation"? It’s basically code written in a certain language to do the bulk of the work described in the previous 6-steps. It exposes its own APIs that your server can use to perform the generic behaviors that are expected of a GraphQL server.

As a GraphQL service developer, you can leverage your GraphQL implementation of choice to do most of the heavy lifting like parsing, validating, and executing GraphQL requests. This enables you to focus on your application logic details. You need to write a "schema" and come up with how the parts in that schema "resolve" to data (and side effects). We’ve designed the AZdev schema in the previous chapter and we’ll start implementing its resolvers in this chapter. However, before we do that, let’s work through a simpler example to understand the basics of running a GraphQL service. Let’s implement the currentTime field as an example.

2.1. Creating the schema object

We’ve already installed the GraphQL.js implementation (the graphql npm package). There are hundreds of top-level objects you can import from this package but for the very first example, we will need to use only two of them:

  1. The function that can build a schema from a schema language text. This is named buildSchema in GraphQL.js.

  2. The function to execute a GraphQL query against that generated schema. This is named graphql in GraphQL.js and to avoid confusion, I’ll refer to it as the "graphql executor function".

Let’s create two files. One to host the schema and resolvers definitions and one to execute the schema using a query text supplied by the user. To keep this example simple, I’ll use a command-line interface to read the query text from the user instead of introducing a more featured user interface (like an HTTP server).

Create a schema directory under /lib and put the following index.js file in there:

Listing 5. 11. New file: lib/schema/index.js
import { buildSchema } from 'graphql';

The buildSchema function takes a string written in the GraphQL schema language which represents a set of "types". Every object in a GraphQL schema must have an explicit type. This starts with the roots of what the schema is offering. For example, to make the schema accept queries in general, you need to define the special "Query" type. To make the schema accept a "currentTime" field in a query operation, you need to add it within the "Query" type and mark it as a "String".

Here’s the schema text needed for the simple example schema we’re building:

Listing 5. 12. In lib/schema/index.js
export const schema = buildSchema(`
  type Query {
    currentTime: String!
  }
`);

The string in Code Listing 5.3 (the bolded part) is the schema language text. Note the use of backticks to allow for having the text on multiple lines.

The result of executing buildSchema will be a special JavaScript object that’s designed to work with the graphql executor function.

2.2. Creating resolver functions

We have a schema and we can validate any request against it if we need to, but we have not told the GraphQL service what data to associate with the one field in that schema. If a client asks for the current time, what should be the server response?

This is the job of a "resolver function". Each field defined in the schema (like currentTime) needs to be associated with a resolver function. When it is time for the server to reply with data for that field, it will just execute that field’s resolver function and use that function’s return value as the data response for the field.

Let’s create an object to hold the many resolver functions we’ll eventually have. Here’s one way to implement the currentTime resolver logic:

Listing 5. 13. In lib/schema/index.js
export const rootValue = {
  currentTime: () => {
    const isoString = new Date().toISOString();
    return isoString.slice(11, 19); (1)
  },
};
1 The ISO format is fixed. The 11-19 slice is the time part.

This rootValue object will have more functions as we add more features to the API. It’s named "rootValue" because GraphQL.js use it as the root of the graph. Functions within the rootValue object are the resolvers for the top-level nodes in your graph.

You can do anything you wish within a resolver function! For example, you can query a database for data. This is what we need to do for the AZdev API, but let’s finish the currentTime example field first.

Note how I exported the schema and rootValue object. Other modules in the server will need to import and use these objects.

2.3. Executing requests

The schema and rootValue objects are the core elements in any GraphQL service. You can pass them to the graphql executor function along with the text of a query or mutation and the executor will be able to parse, validate, execute, and return data based on them. This is what need to do next to test the currentTime field.

The graphql executor function (available as a top-level import in GraphQL.js) can be used for this purpose. Create a server.js file under lib and start it out with this line:

Listing 5. 14. New file: lib/server.js
import { graphql } from 'graphql';

This graphql executor function accepts a list of arguments; the first one is a schema object, the second one is a source request (the query/mutation text), and the third one is a rootValue object of resolvers.

graphql(schema, request, rootValue);
The graphql executor function has more positional arguments that can be used for advanced cases. However, we’ll eventually use an HTTP(S) wrapper to run this function instead of calling it directly and we’ll use named arguments when we do.

The graphql executor function returns a promise. In JavaScript, we can access this promise resolved value by putting the keyword await in front of it and wrapping the code with a function labeled with the async keyword:

Listing 5. 15. The async/await pattern
async () => {
  const resp = await graphql(schema, request, rootValue);
};

The promise resolves to the GraphQL response in JSON. Each GraphQL response has a "data" attribute that holds any successfully resolved data elements (and an "error" attribute if errors are encountered). Let’s just print the resp.data attribute.

console.log(resp.data);

For the 3 arguments of the graphql executor function, we can import the schema and rootValue objects from the previous file we worked on but where do we get the request text?

The request text is something the clients of this API server will supply. They’ll do that eventually over an HTTP(s) channel but for now we can read it from the command line directly as an argument. We’ll test the server.js file this way:

Listing 5. 16. Command to test lib/server.js
$ node lib/server.js "{ currentTime }"

For this test, the request text is the third argument in the command line. You can capture that in any Node script with process.argv[2].

In Node, "process.argv" is a simple array that has an item for each positional token in the command like that ran the process starting with the command itself. For the command in Listing 5.7, process.argv is ["the/path/to/node/command", "lib/server.js", "{ currentTime }"].

Here’s the full text in lib/server.js that we can use to carry out this test:

Listing 5. 17. lib/server.js | az.dev/gia
import { graphql } from 'graphql';
import { schema, rootValue } from './schema/index.js';

const executeGraphQLRequest = async request => {
  const resp = await graphql(schema, request, rootValue);

  console.log(resp.data);
};

executeGraphQLRequest(process.argv[2]);

We simply import the schema and rootValue that we prepared, wrap the graphql executor function in an async function, and use process.argv[2] to read the GraphQL request text from the user.

This example is complete! you can test it with command in Listing 5.7 and you should see the server reporting the time in UTC.

ch05 fig 01 gqlia
Figure 5. 2. Testing a GraphQL service from the command line

The GraphQL.js implementation uses null-prototyped objects for data responses. This is why [Object: null prototype] was part of the response. The console.log function in Node reports that when it sees it. Null-prototyped objects are generally better for maps/lists because they start empty and do not inherit any "default" properties. For example, you can do ({}).toString() but you can’t do Object.create(null).toString().

3. Communicating over HTTP

Before adding more fields to this API, let’s use a better interface for testing queries and mutations than the simple command-line interface. Let’s communicate with the GraphQL service via HTTP. To do that, we need an HTTP server.

You should host your GraphQL services behind an HTTPS service. You can use Node to create an HTTPS server but I think a better option is to use a web server like NGINX (or a web service like CloudFlare) to protect your HTTP service and make it available only over HTTPS.

We’re going to use the "express" package to create an HTTP server and the "express-graphql" package to wire that server to work with the GraphQL service that we have so far. We’ve installed these dependencies already so the next thing we need to do is import them somewhere.

Replace existing content in lib/server.js with these imports:

Listing 5. 18. New content of lib/server.js | az.dev/gia
import express from 'express';
import graphqlHTTP from 'express-graphql';
import { schema, rootValue } from './schema/index.js';
Although it’s named "express-graphql", this package can work with any HTTP web framework that supports connect-style middleware (like Hapi, Fastify, etc).

The default export in the express package is a function. To create an express server, you just invoke that function. Then you can use the listen method on the created server to make the server listen to incoming connections on a certain port:

Listing 5. 19. In lib/server.js | az.dev/gia
const server = express();

server.listen(4321, () => {
  console.log('API server is running');
});

When you run this code, an HTTP server will be listening on port 4321 but that server still has no instructions about what to do when incoming connections are received. To make the server accept incoming connections for a certain HTTP URL/VERB combination (like "GET /") we need to add a server.get method (or .post/.put/.delete) or the generic server.use method which makes the server accept all HTTP VERBS for a certain URL.

This is the signature of the server.VERB methods and an example of what you can do within it:

Listing 5. 20. Express API to define a route and its handler
server.use('/', (req, res, next) => {
  // Read something from req
  // Write something to res
  // Either end things here or call the next function
});

The first argument to the .use method is the URL for which the server will start accepting connections. The second argument is the function that will be invoked every time the server accepts a connection on that URL. This function is usually called the "listener" function.

The listener function exposes 2 important objects as arguments. The req and res objects (the next object is not usually used for response handlers).

  1. The req object is how the service can read information from the HTTP request. For example, we need to read the text of the query/mutation (and other related objects) from a client who is using this API. We can do that using the req object.

  2. The res object is how the service can reply with data to the client who is requesting it. This is how the API server respond with the data it generates for incoming GraphQL requests.

Between reading from the request and writing to the response, we’ll need to execute the graphql executor function just like we did for the command-line test.

This all will happen for each GraphQL request and it’s another general process that can be abstracted and re-used. The default export of the "express-graphql" package (which we imported as graphqlHTTP) is a listener function that will do exactly that. It’ll parse the HTTP request, run the graphql executor function, await on its response, and send its resolved data back to the requester. We just need to tell it what schema and rootValue objects to use.

Here’s the .use method wired to work with the graphqlHTTP helper function:

Listing 5. 21. Mounting a GraphQL service under an HTTP route | az.dev/gia
server.use(
  '/',
  graphqlHTTP({
    schema,
    rootValue,
    graphiql: true,
  })
);

That’s it. This will make us able to communicate with the schema over HTTP. Not only that, by using graphiql: true in the configuration object we’ll also get the mighty GraphiQL editor mounted on that URL and it’ll work with our schema!

The graphqlHTTP function call returns a handler function that expects the 3 arguments (req, res, next) and that matches the signature needed for the use method’s handler function (its second argument).

Let’s test. Run lib/server.js with the node command:

$ node lib/server.js

You should see the message:

API server is running

Then head over to http://localhost:8484/ and you should see the GraphiQL editor there and you should be able to test the currentTime field query in it:

ch05 fig 02 gqlia
Figure 5. 3. express-graphql has the GraphiQL editor built-in

Note that the whole HTTP channel to communicate with the server has nothing to do with the GraphQL service itself. It’s just another service layer offering a convenient way to communicate with the GraphQL service layer. A Web application can now use Ajax requests to retrieve data from the GraphQL service. In a large-scale GraphQL API service, this HTTP communication layer would be a separate entity that’s managed and scaled independently.

You can turn off the GraphiQL editor in production (if you want) and use .post instead of .use for the graphqlHTTP handler. That way service will only work for Ajax post requests.

4. Building a schema using constructor objects

The GraphQL schema language is a great programming-language-agnostic way to describe a GraphQL schema. It’s a human-readable format that’s easy to work with and it is the popular and preferable format for describing your GraphQL schemas. It does however have its limitations.

GraphQL.js has another format that can be used to create schemas. Instead of strings written with the schema-language you can use JavaScript objects instantiated from calls to the various constructor classes that are available in the GraphQL.js API. Classes like GraphQLSchema, GraphQLObjectType, GraphQLUnionType, and many others.

This format is useful if you need to construct a schema programmatically and it’s more flexible and easier to manage and extend.

The method of using objects to create a GraphQL schema does not have a good name out there. I’ve heard "code-first" and "resolvers-first" and I don’t think these names fairly represent the method. I’ll refer to it in this book as the "object-based method". I’ll refer to the text-based method as the "full-schema text method".

Let’s start exploring this object-based method by converting the schema we have so far (which only supports a currentTime field).

4.1. Auto-restarting Node

Before we proceed, because we’ll be frequently changing the code on the server, and because we need to restart the "node" command every time we do, let’s use the "nodemon" npm packages to automate this. Nodemon runs a node process while monitoring files for change and automatically restarts that node process when it detects changes to the files.

Nodemon is already installed, to use it, replace the node command with the nodemon command. Let’s create an npm run script to host this new command to run the server for development.

Change the scripts attribute in package.json to:

Listing 5. 22. In package.json
{
   ·-·-· 

  "scripts": {
    "dev-server": "nodemon lib/server.js"
  },

   ·-·-· 
}

Now instead of running the nodemon command directly, to run the web server you run:

$ npm run dev-server

This will run the server using nodemon and now when you save any file in the project the node process will be automatically restarted.

You can add as many npm run scripts as you need and you should use them for any "tasks" you wish to introduce to the project. With npm run scripts, all developers on the team can run these tasks in a standard and consistent way.

4.2. The Top-level Query type

Since we’ll no longer use the full-schema text method, you can start by deleting everything we have so far in lib/schema/index.js.

To create a GraphQL schema using the object-based method, we need to import a few objects from the "graphql" package. For this example, we need:

Listing 5. 23. Replace content in lib/schema/index.js
import {
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
  GraphQLInt,
  GraphQLNonNull,
} from 'graphql';

These type-based objects are designed to work together to help us create a schema. For example, to instantiate a schema object, you just do something like (don’t add this yet):

Listing 5. 24. The GraphQL.js API
const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {}
  }),
});

These calls to GraphQLSchema and GraphQLObjectType return special objects that are designed to work with the graphql executor function. Each top-level query field that you need to support need to be part of the GraphQLObjectType associated with the query property that we pass to the GraphQLSchema configuration object (it’s only argument).

Instead of inlining the call to GraphQLObjectType, let’s extract that into its own variable. I’ll name it QueryType. In this type’s fields property, we need to add the currentTime field, specify its type, and include its resolver function. Here’s the code we need:

Listing 5. 25. In lib/schema/index.js
const QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    currentTime: {
      type: GraphQLString,
      resolve: () => {
        const isoString = new Date().toISOString();
        return isoString.slice(11, 19);
      },
    },
  },
});

export const schema = new GraphQLSchema({
  query: QueryType,
});
Don’t memorize things but rather understand and retain the capabilities these constructor and type helper objects enable you to do.

An object-type has a name and a list of fields (represented with an object). Each field has a type property and a resolve function.

This maps to the schema-language version we had before. We’re just doing it with objects instead of strings. Instead of "currentTime: String", this method requires defining a property currentTime, and giving it a configuration object with a type of GraphQLString (and an extra resolve property).

I used the GraphQLString scalar type for currentTime. The GraphQL.js implementation offers a few other scalar types like this one, including GraphQLInt, GraphQLBoolean, and GraphQLFloat.

The resolver function is exactly the same one we had under the rootValue object before except now it’s part of the schema object itself. Using the object-based method, we don’t need a rootValue object because all resolvers are included were they’re needed alongside their fields. The schema object created with GraphQLSchema is "executable" on its own.

To test this code, we’ll need to remove the rootValue concept from lib/server.js:

Listing 5. 26. Changes in lib/server.js (in bold)
import express from 'express';
import graphqlHTTP from 'express-graphql';
import { schema } from './schema/index.js';

const server = express();
server.use(
  '/',
  graphqlHTTP({
    schema,
    graphiql: true
  })
);

server.listen(8484, () => {
  console.log('API server is running');
});

That’s it. Nodemon should restart the process in the background once you save these changes and you can test in GraphiQL that the service still supports the same currentTime field, but now using the object-based method.

4.3. Field arguments

To explore the GraphQL.js API further, let’s make the API support a sumNumbersRange field that accepts two arguments (begin and end) representing a range of numbers and return the sum of all whole numbers in that range (inclusive to its edges). This is the desired end result:

ch05 fig 03 gqlia
Figure 5. 4. The sumNumbersInRange field

This is an abstract example just to get our feet wet with the object-based method for working with GraphQL schemas but you can think of it as the backend to support "captcha" style questions for form validation. Let’s imagine when you login to AZdev, the form will ask you something like: what’s the sum of whole numbers in the range from 2 to 5?

Here’s a simple implementation of the sumNumbersInRange field. You’ll need to add this to the fields property for QueryType:

Listing 5. 27. In lib/schema/numbers-range.js
  fields: {
    // ·-·-·

    sumNumbersInRange: {
      type: new GraphQLNonNull(GraphQLInt),
      args: {
        begin: { type: new GraphQLNonNull(GraphQLInt) },
        end: { type: new GraphQLNonNull(GraphQLInt) },
      },
      resolve: function(source, { begin, end }) {
        let sum = 0;
        for (let i = begin; i <= end; i++) {
          sum += i;
        }
        return sum;
      },
    },
  },

The sumNumbersInRange field has a type of new GraphQLNonNull(GraphQLInt). The GraphQLNonNull wrapper around this integer type indicates that this field will always have a value. The response of a sumNumbersInRange field in a query will never be "null". This is what the exclamation mark after types represented in the full-schema text method.

The definition of sumNumbersInRange included an args property to define the structure of the arguments it accepts and their types (which I defined using new GraphQLNonNull(GraphQLInt) as well). Both of these arguments are required. A client cannot ask for the sumNumbersInRange field without specifying the begin and end numbers for that range. The GraphQL service will throw an error when that happens.

The resolver function for sumNumbersInRange makes use of its arguments. The first argument is always the source "parent" object of that resolved level. For sumNumbersInRange, there is no parent object because this is a root field. The second argument to the resolve function exposes the field arguments values as used in the client query. I destructured begin and end from that as both of these arguments are required. The function simply loops over the range, compute the sum, and return it.

Test the new field this API now supports with this query:

Listing 5. 28. The sumNumbersInRange field
{
  sumNumbersInRange(begin:2, end:5)
}

Note how the sumNumbersInRange field has no sub selection set because it’s a leaf field that resolves to a scalar value. However, to learn about custom object types, let’s change it to a non-leaf field that requires a sub selection set. We’ll do that next.

The GraphQLNonNull helper is the GraphQL.js way to specify a "type modifier" and it’s equivalent to adding an exclamation mark to the type in the full-schema text method. The equivalent of adding square brackets to make a list is the GraphQLList type modifier. For example, to define a field that represents an array of strings, the type would be new GraphQLList(GraphQLString).

You can also combine these two type modifiers as we’ll need to do in many of the upcoming examples.

4.4. Custom object types

So far we’ve created one object-type to represent the top-level fields under the query type. To explore using custom object types, let’s replace the sumNumbersInRange leaf field with a "numbersInRange" object field that supports the same begin/end arguments and let’s make it support 2 leaf field for the sum and count of the whole numbers in the range (this will give the captcha-style form validation feature more question options!).

Here’s how the new numbersInRange field can be queried:

{
  numbersInRange(begin: 2, end: 5) {
    sum
    count
  }
}

To accomplish this, we need to define a custom object type to represent the new "numbers in range" structure which looks like an object that has a "sum" and "count" properties. Because this will be a new type in the API and to start organizing the many files we’ll have in this API, let’s create a new file for the type of numbersInRange. Create a new folder under lib/schema/types. We’ll use this to host one file for each new type we’ll introduce. Starting with NumbersInRange:

Listing 5. 29. New file: lib/schema/types/numbers-in-range.js
import { GraphQLObjectType, GraphQLInt, GraphQLNonNull } from 'graphql';

const NumbersInRange = new GraphQLObjectType({
  name: 'NumbersInRange',
  description: 'Aggregate info on a range of numbers',
  fields: {
    sum: {
      type: new GraphQLNonNull(GraphQLInt),
    },
    count: {
      type: new GraphQLNonNull(GraphQLInt),
    },
  },
});

export default NumbersInRange;

The "NumbersInRange" name is the friendly name of this type. The description field is an optional text that you can use to describe this type (which maps to the triple quote line we’ve seen in the full-schema text method). Once this new NumbersInRange type is used in the main schema, both of its name and description fields will show up in tools like GraphiQL to facilitate working with it. Use descriptive names and helpful descriptions with all your custom types.

ch05 fig 04 gqlia
Figure 5. 5. How name/description show up in GraphiQL documentation explorer
Descriptions are the core elements of the built-in documentation mentality in GraphQL. You can add them to main types, fields, and even arguments. Most of these elements should have descriptions. You can also use rich-text formats like Markdown or Asciidoc in these descriptions and then have the client tool render them in a more readable way. GraphiQL supports rendering Markdown in descriptions out of the box!

Note how the sum and count fields in the NumebrsInRange type did not have resolver functions. Although this design made the sum and count fields into leaf ones, having resolver functions for them is optional. This is because these leaf fields can use the default trivial resolvers based on properties defined on their parent source object. For this to work the object resolved as the parent object (which is of type NumbersInRange) has to respond to sum and count methods.

Let’s create a function that takes begin and end as arguments, computes the sum/count, and returns an object with sum and count properties. Here’s one way to implement that (you can put that in a lib/utils.js file):

Listing 5. 30. Helper function to create a range object. In lib/utils.js.
export const numbersInRangeObject = (begin, end) => {
  let sum = 0;
  let count = 0;
  for (let i = begin; i <= end; i++) {
    sum += i;
    count++;
  }
  return { sum, count };
};

There is a better way to compute the count and sum of consecutive numbers in a range without using a loop. You can use the "arithmetic progression" formulas. I only used a loop for simplicity.

Now we need to change the top-level QueryType object. It now has a non-leaf field named numbersInRange and that field needs to be resolved with an object returned by calling the numbersInRangeObject helper function.

Listing 5. 31. Changes in lib/schema/index.js (in bold)
// ·-·-·

import NumberInRange from './types/numbers-in-range.js';
import { numbersInRangeObject } from '../utils.js';

const QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    // ·-·-·

    numbersInRange: {
      type: NumberInRange,
      args: {
        begin: { type: new GraphQLNonNull(GraphQLInt) },
        end: { type: new GraphQLNonNull(GraphQLInt) },
      },
      resolve: function(source, { begin, end }) {
        return numbersInRangeObject(begin, end);
      },
    },
  },
});

// ..

That’s it. If you test the API now, you should be able to execute a query like:

{
  numbersInRange(begin: 2, end: 5) {
    sum
    count
  }
}

And get this response:

{
  "data": {
    "numbersInRange": {
      "sum": 14,
      "count": 4
    }
  }
}
Challenge: Add an avg field to the NumbersInRange type and make it return the sum divided by the count.

4.5. Custom errors

A GraphQL executor will automatically handle any invalid queries or types used for arguments in the query. For example, if you omit one of the required (Non-Null) arguments, you get:

ch05 fig 05 gqlia
Figure 5. 6. All required arguments need to be present in the request

If you use strings instead of integers for begin/end, you get:

ch05 fig 06 gqlia
Figure 5. 7. Only the right data types are accepted

If you attempt to query for a non-existing leaf field, you get:

ch05 fig 07 gqlia
Figure 5. 8. Only fields published by the schema can be used

This is the power of a strongly-typed schema. You get many great validations out of the box, but what about the custom cases? What should happen if a requester specifies an invalid range for the numbersInRange field (for example, end is an integer but it’s less that the begin integer)? The API currently ignores this case and just returns 0s:

ch05 fig 08 gqlia
Figure 5. 9. To error or not to error

Let’s fix this. Let’s change the API to reject this input and instead of returning 0s return a custom error message to the requester. If the range is invalid the requester should be made aware of that because otherwise unintentional bugs might sneak into the code.

We simply do the check in the resolver function for the numbersInRange field and throw an error with our custom message:

Listing 5. 32. Changes to the numbersInRangeObject object (in bold)
export const numbersInRangeObject = (begin, end) => {
  if (end < begin) {
    throw Error(`Invalid range because ${end} < ${begin}`);
  }
  // ·-·-·
};

Now if we attempt to make an invalid range query, you get:

ch05 fig 09 gqlia
Figure 5. 10. A custom error message in the response

Note how the errors are delivered as part of the JSON response (and not through HTTP error codes for example). In some cases, the JSON response might have both errors and partial data that is not affected by the errors. You can test that out by including the currentTime field to a query with a bad range for numbersInRange:

ch05 fig 10 gqlia
Figure 5. 11. Note how the response has both errors and data

Did you notice how I made the numbersInRange field nullable in Listing 5.30? For this particular case, a numbersInRange field might be "absent" from the response when the range it uses is invalid. This is another example of a case where nullability is okay because I am attaching a semantic meaning to it. Also, because the numbersInRange field is a root one, making it non-nullable will prevent having a partial response in other root fields (like currentTime) when there is an error in the range. Try this as an exercise.

5. Objects To schema language conversion

The executable schema object that we created using the object-based method can be converted to the schema-language format using the printSchema function which is available as a top-level import from the "graphql" package, then call it passing in the executable schema object (the one exported by lib/schema/index.js) as the argument:

Listing 5. 33. Changes to lib/schema/index.js (in bold)
import {
  // ·-·-·
  printSchema,
} from 'graphql';

// ·-·-·

export const schema = new GraphQLSchema({
  query: QueryType,
});

console.log(printSchema(schema));

Here’s what you’ll see:

"""Aggregate info on a range of numbers"""
type NumbersInRange {
  sum: Int!
  count: Int!
}

type Query {
  currentTime: String
  numbersInRange(begin: Int!, end: Int!): NumbersInRange
}

This is the schema representation without the resolver functions but it is a lot more concise and readable.

My favorite part about this conversion is how the arguments to the numbersInRange field are defined in the schema language format:

(begin: Int!, end: Int!)

Compare that with:

args: {
  begin: { type: new GraphQLNonNull(GraphQLInt) },
  end: { type: new GraphQLNonNull(GraphQLInt) },
},

Note how the description of NumbersInRange was included right before it and it was surrounded by a set of 3 double quotes. Here’s an example of a well-described version of the API we have so far:

Listing 5. 34. Using descriptions in the schema language
"""Aggregate info on a range of numbers"""
type NumbersInRange {
  "Sum of all whole numbers in the range"
  sum: Int!
  "Count of all whole numbers in the range"
  count: Int!
}

"""The root query entry point for the API"""
type Query {
  """
  An object representing a range of whole numbers
  from "begin" to "end" inclusive to the edges
  """
  numbersInRange(
    "The number to begin the range"
    begin: Int!,
    "The number to end the range"
    end: Int!
  ): NumbersInRange!

  "The current time in ISO UTC"
  currentTime: String
}

5.1. The full-schema text vs the object-based methods

The schema language is a great programming-language-agnostic way to describe a GraphQL schema. It’s a human-readable format that’s easy to work with. The frontend people on your team will absolutely love it. It enables them to participate in the design of the API and, more importantly, start using a mocked version of it right away. The schema language text can serve as an early version of the API documentation.

However, completely relying on the full schema text to create a GraphQL schema has a few drawbacks. You’ll have to put in some effort to make the code modularized and clear and you have to rely on coding patterns and tools to keep the schema-language text consistent with the tree of resolvers (AKA resolvers map). These are solvable problems.

The biggest problem I see with the full-schema text method is that you lose some flexibility in your code. All your types have to be written in that certain way that relies on the schema language text. You can’t use constructors to create some types when you need to. You’re locked down to this string-based approach. Although the schema language text makes your types more readable, in many cases you’ll need the flexibility over the readability.

The object-based method is flexible and easier to extend and manage. It does not suffer from any of the mentioned problems. You have to be modular with it because your schema will be a bunch of objects. You also don’t need to merge modules together because these objects are designed and expected to work as a tree.

The only problem I see with the object-based method is that you have to deal with a lot more code around what’s important to manage in your modules (types and resolvers). A lot of developers see that as "noise" and you can’t blame them.

If you’re creating a small-scope and well-defined GraphQL service, using the full-schema-string method is probably okay. However, in bigger and more agile projects I think the more flexible and more powerful object-based method is the way to go.

You should still leverage the schema-language text even if you’re using the object-based method. At jsComplete.com for example, we use the object-based method but every time the schema is built we use the printSchema function to write the complete schema to a file. We commit and track that file in the Git repository of the project and that proved to be a very helpful practice!

6. Working with asynchronous functions

Both fields we have so far in this example are mapped to normal synchronous resolver. This means if one of them takes a long time to execute, the whole API service will be blocked and not able to serve other requests! This is bad.

To demonstrate this problem, let’s fake a delay in processing the currentTime field. JavaScript has no sleep function but it’s easy to accomplish something similar by comparing dates. Here’s one way to make the currentTime resolver function synchronously take 5 seconds to complete:

Listing 5. 35. Delay returning from the currentTime resolver function by 5 seconds
currentTime: {
  type: GraphQLString,
  resolve: () => {
    const sleepToDate = new Date(new Date().getTime() + 5000);
    while (sleepToDate > new Date()) {
      // sleep
    }
    const isoString = new Date().toISOString();
    return isoString.slice(11, 19);
  },
},

Now each time you ask for the currentTime field, the server will spend 5 seconds doing nothing and then it will return the answer. The problem is that during these 5 seconds the whole node process for the server is completely blocked. A second requester can’t get any data from the API until the while loop in the first request is done.

ch05 fig 11 gqlia
Figure 5. 12. The second request (right side) is waiting on the first one

You should never do that. Instead, all long running processes should be done asynchronously either with native APIs offered by Node and its many packages, or by forking the work over to a worker thread/process.

For example, to make the currentTime field fake-delay its response by 5 seconds but asynchronously, we can use the setTimeout method and wrap it in a promise object:

Listing 5. 36. Returns a promise that’ll resolve to the current time after 5 seconds
const currentTimeAsync = () =>
  new Promise(resolve => {
    setTimeout(() => {
      const isoString = new Date().toISOString();
      resolve(isoString.slice(11, 19));
    }, 5000);
  });

This currentTimeAsync function returns a promise. We now need the GraphQL executor to "await" on this promise and return the data to the requester only when that promise resolves with its data. Luckily, this behavior is built into the GraphQL.js implementation itself. A resolver function can return a promise and the executor will just do the right thing for it.

Listing 5. 37. Resolver functions support returning promise objects
currentTime: {
  type: GraphQLString,
  resolve: () => {
    return currentTimeAsync();
  },
};

Now each time you ask the API service for the currentTime field, it’ll still answer after 5 seconds but the service process is not blocked! Other requesters can ask for other parts of the API and get immediate responses while a requester is waiting for the currentTime:

ch05 fig 12 gqlia
Figure 5. 13. The second requester (right side) can get a response while the first one is waiting

This is going to be very handy when we work with objects coming from databases because we should definitely use the asynchronous APIs to make all the communications with all the databases. I think we’re ready for that!

If you want to keep the currentTime field in this schema, undo the fake delay that we just did for the sake of an example.

7. Designing the PostgreSQL database models

Let’s start with the main database models that are to be hosted in PostgreSQL.

You need to create a local PostgreSQL database and connect to it before running any commands in psql (or any other PostgreSQL interfaces). Creating a database, connecting to it, and running commands on it will be different based on your OS or service. Go ahead and do that now before proceeding with this chapter.

For the rest of this section, I’ll be assuming that you are in a PostgreSQL interface (like psql) and you can run SQL statements and commands on a database.

First, it’s a good idea to create a "schema" in a PostgreSQL database to host an application’s tables rather than have them in the default "public" schema. This way you can use the same PostgreSQL database to host data for many applications. This is especially helpful if you have a group of schemas that are related to each other.

A PostgreSQL schema has nothing to do with a GraphQL schema. It’s just a way to organize tables and views (and other objects) in a PostgreSQL database.

To create a PostgreSQL schema, you can use this command:

CREATE SCHEMA azdev;

7.1. The User model

The "users" database table will have a record for each registered user. Besides the unique id and creation-time fields we’re adding under each model, a user record will need to have a unique email field and a hashed password field. These fields are required.

A registered user can also have a first and last name but let’s make these nullable. This way a front-end application can have users register with just their email (and password) and update their profile information later.

A registered user can also be an admin. Admin users will be given access to edit and delete any Need or Approach records. We’ll introduce a boolean flag to indicate this special access attribute. This flag should have a default value of false.

Finally, we need a mechanism to authenticate requests to the GraphQL API after a user logs in without having them send over their password each time. We’ll manage that with a column to store temporary auth tokens which should be hashed as well.

Here’s a SQL statement you can use to create this table with all of these columns:

Listing 5. 38. The "azdev.users" table | az.dev/gia
CREATE TABLE azdev.users (
  id serial PRIMARY KEY,

  email text NOT NULL UNIQUE,
  hashed_password text NOT NULL,

  first_name text,
  last_name text,

  is_admin boolean NOT NULL DEFAULT false,

  hashed_auth_token text,

  created_at timestamp without time zone NOT NULL
    DEFAULT (now() at time zone 'utc'),

  CHECK (lower(email) = email)
);

Note how I gave the id field the serial type and the PRIMARY KEY constraint. The serial type will automatically fill this field using a sequence of integers (which gets automatically created and managed for this table). The PRIMARY KEY constraint will make sure that values in this column are not-null and unique. We’ll need the exact same id column definition in all tables as they will be used for referential integrity constraints (making sure records reference existing records).

The email field is also UNIQUE and NOT NULL, making it practically another PRIMARY KEY. If you want to use the email as a PRIMARY KEY (and that’s not too bad of an idea), you just need to make sure any referential integrity constraints are updated correctly if the user decides to change their email address. PostgreSQL has some advanced features to help with that.

The created_at field will also be automatically populated by PostgreSQL itself but through the DEFAULT keyword this time. It’ll store the time each record was created in the UTC time zone. Both the id and created_at fields will not be mutated by the GraphQL API. Clients can just read them if they need to.

KEEP IT SIMPLE! I find it a lot easier to store date-time values without time zone information and always store UTC values in them. Life is too short to deal with time-zoned date-time values and their conversions.

The CHECK constraint on the "email" field validates that emails will always be stored in their lowercase form. This is a good practice to do for fields that are unique regardless of their case. I’ve learned that the hard way.

The "hashed_auth_token" field is needed to authenticate requests to the GraphQL API after a user logs in. Because HTTP APIs are stateless, instead of having the user send over their password with every GraphQL operation, once they log in successfully we’ll give them a unique API "key" for them to use in subsequent GraphQL requests and the server will use that key to identify the logged in user. The auth token value is like a temporary password. It can be renewed per session, for example, and we could come up with a way to invalidate it after a certain time.

There will always be more things you can do to make an API more secure but, for the purposes of this book, we’ll keep things simple but still go with the practical minimum. For example, don’t ever store plain-text passwords or access tokens in your database! Even encrypting them is not secure enough. You should one-way hash them. That’s why I named these fields using the hashed_ prefix.

Note how I used snake-case (underscore separator) for PostgreSQL column names and not came-case like GraphQL fields. PostgreSQL column names are case-insensitive (unless you use quotes). So if we named the column createdAt it’ll be converted to createdat. The snake-case style is the common convention in PostgreSQL and it will add some challenges for us down the road when we need to map these columns to GraphQL camel-case fields.

7.2. The Need/Approach models

The "needs" table will have a record for each Need entry that’s submitted to the AZdev application.

A Need object can have many tags. We could come up with a new "tags" table and introduce a new many-to-many relation to the "needs" table but let’s just store these tags as a comma-separated value in each Need record row. This will be simpler on the database side but limiting on the frontend. However, remember that GraphQL types don’t need to match their source of data so we can still expose this string tags field to the API users as an array of strings instead.

PostgreSQL has an advanced feature to manage a list of items for a single row. A PostgreSQL column can have an "array" data type! I am using the comma-separated value to keep things simple here but feel free to experiment with that array data type and see how to deal with it once we start mapping PostgreSQL columns to GraphQL fields.

A Need (or Approach) record has to belong to a User record because only logged-in users can submit new entries. For that, we can use a FOREIGN KEY constraint to validate the map between a Need and a User. We’ll need to do the same for Approaches as well.

Let’s add an is_featured column to allow a Need record to be featured (and listed on the home page of AZdev). Let’s also add an is_public column to allow users to have their own private entries that are not featured to the public.

Here’s a SQL statement we can use to create the "needs" table.

Listing 5. 39. The Needs table | az.dev/gia
CREATE TABLE azdev.needs (
  id serial PRIMARY KEY,

  content text NOT NULL,
  tags text,

  user_id integer NOT NULL,

  is_public boolean NOT NULL,
  is_featured boolean NOT NULL DEFAULT FALSE,

  created_at timestamp without time zone NOT NULL
    DEFAULT (now() at time zone 'utc'),

  FOREIGN KEY (user_id) REFERENCES azdev.users,
  CHECK (is_public = true OR is_featured = false)
);
Challenge: What is the CHECK constraint on the needs table validating?

The "approaches" table will have a record for each Approach submitted on a Need entry. An Approach record is just another content text field. Each Approach record has to be stored under a valid User record and mapped to a Need record. This table will have two FOREIGN KEY constraint columns: user_id and need_id.

We are also planning to support voting on Approaches and sorting Approaches based on their vote count. Let’s add a vote_count column on the "approaches" table and make it default to 0. The approachVote mutation will either increment or decrement this column.

Here’s the SQL statement that we need:

Listing 5. 40. The Approaches table | az.dev/gia
CREATE TABLE azdev.approaches (
  id serial PRIMARY KEY,

  content text NOT NULL,

  user_id integer NOT NULL,
  need_id integer NOT NULL,

  vote_count integer NOT NULL DEFAULT 0,

  created_at timestamp without time zone NOT NULL
    DEFAULT (now() at time zone 'utc'),

  FOREIGN KEY (user_id) REFERENCES azdev.users,
  FOREIGN KEY (need_id) REFERENCES azdev.needs
);
ch04 fig 03 gqlia
Figure 5. 14. The UML diagram for the 3 tables in PostgreSQL

Did you notice how I’ve used many database constraints like PRIMARY KEY, NOT NULL, UNIQUE, CHECK, and FOREIGN KEY? These database constraints will help future developers (including you) understand the design decisions you’re making today and they’ll be the last-standing guard if a user (or a program) tries to insert invalid data to the database. When it comes to data integrity, spare no layers! The least you can do is have the database validate it. Don’t skip that. You should also add more layers to give users of your API more meaningful error messages when they attempt to insert invalid data. We’ll do some data validation in the GraphQL layer as well.

7.2.1. The Need/Approach GraphQL types

The Approach type has to expose the content column and the voteCount column. The Need type has to expose the content column and the tags column. For the tags value, instead of having its type match the database, let’s make the API service expose it as an array of strings.

Here is what we need to change on the Need/Approach GraphQL types:

Listing 5. 41. GraphQL type for the Need and Approach models
type Approach implement Node {
  id: ID!
  createdAt: String!
  content: String!
  voteCount: Int!
  author: User!
  detailList: [ApproachDetail!]!
}

type Need implement Node {
  id: ID!
  createdAt: String!
  content: String!
  tags: [String!]!
  author: User!
  approachList: [Approach!]!
}

Note how I did not include a field that maps directly to the user_id column in both models. The API has the author field here as an object, not authorId or userId as scalar values (like the database). This is one of the powerful aspects of GraphQL. We don’t give API consumers the database-id values (although that’s possible if needed), but rather the whole object represented by that id value under that relation. The same is true for the approachList and detailList fields. These are arrays of objects not ids.

Also note that I did not expose the featured/public flags on a Need record because these are not needed in the UI at all (at least not in the first version).

Remember to always think of the GraphQL type in terms of what will exactly be needed by the UI that’s going to use this type. It’s easy to add features when you need them. It’s a lot harder to remove them.

8. Designing the MongoDB models

Let’s now design the sources of data that are to be hosted in MongoDB. Before doing that, we need to create a new database for AZdev.

In MongoDB, there is no "schema" concept to group related database entities. You just create a database for that purpose. You actually don’t need to "create a database". You just "use" it and MongoDB will automatically create the currently-used database the first time you insert any data to it.

For the instructions in this section, I am going to assume that you’re in the MongoDB CLI tool which you can open with the mongo command:

$ mongo

I’ll use the $ sign to mean that this is a "command" to be executed in a "terminal". The $ sign is not part of the command.

Once in a mongo session, you can run the following command to "use" a new database.

use azdev

Note that this will not create the azdev database yet.

A "Model" is represented with a "Collection" object in MongoDB and, just like the database itself, you don’t need to create a collection to be able to store documents. MongoDB will accept requests to store any data in any form or shape whether a collection for it existed before or not. For new types of documents, MongoDB will automatically create new collections.

The flexibility in document databases is great but it also might be a source of big problems. A simple typo might lead to having a brand new (wrong) collection in the database. Be careful with that. With flexibility comes great responsibility!

8.1. The Approach details collection

This is actually the only collection we need for the first version of this API. It’ll be used to store dynamic data elements on Approaches like explanations, warnings, or notes.

You can create empty collections in MongoDB if you want and you can also restrict the privileges of a database user to only do certain actions on certain collections in certain databases! I think that’s a great way to validate that data will be stored in its intended locations. I’ll skip the privileges part here but let’s plan the collection for the extra dynamic data elements that we want to support on Approaches.

MongoDB supports performing some data validation when inserting (or updating) documents in collections. This is useful when there are certain fields in your documents that cannot be empty, have to have a certain "type", or even have a certain structure. For an Approach entry to have extra data elements in MongoDB, we need to associate its MongoDB record with its PostgreSQL "ID" to be able to do the mapping between the two sources.

Let’s use MongoDB schema validation to make sure we have that mapping for each Approach document. Here’s the MongoDB command you can use to create the "approachDetails" collection and define its "validator" that checks for the existence of a numeric pgId field:

Listing 5. 42. The "approachDetails" collection | az.dev/gia
db.createCollection("approachDetails", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["pgId"],
      properties: {
        pgId: {
          bsonType: "int",
          description: "must be an integer and is required"
        },
      }
    }
  }
});

The command above will create an approachDetails collection and because it’s the first thing we’re creating in the currently-used azdev database it’ll also create that database as well. You can verify that with the show dbs command. The show collections command should report back: approachDetails.

ch04 fig 04 gqlia
Figure 5. 15. The UML diagram for the Approaches collection

Each Approach record will have a single record in the aproachDetails collection. The Approach Detail record will have fields like explanations, warnings, notes, and other categories in the future. Each of these fields will have an array of text items. We’ll have to transform this special storage schema when resolving a GraphQL API request that asks for Approach Details.

Think of adding more tables and collections to the azdev database. For example, maybe store vote records and track who voted what and when. I’ll leave that as an exercise for you if you want to expand the scope of this API and make it more challenging.

9. Summary

  • Seed your test databases with realistic production-like data to make your tests relevant and useful.

  • A GraphQL service is centered around the concept of a schema that is made executable with resolver functions.

  • A GraphQL implementation like GraphQL.js takes care of the generic tasks around working with an executable schema.

  • You can interact with a GraphQL service with any communication interface. HTTP(S) is the popular choice for GraphQL services that are designed for Web and mobile applications.

  • The GraphQL.js implementation offers a programmatic way to create an executable schema.

  • You can convert from one schema representation to another using GraphQL.js helper functions like buildSchema and printSchema.

  • Resolver functions in the GraphQL.js implementation can work with asynchronous promise-based operations out of the box.

  • You should never do long-running processes synchronously because that will block the GraphQL service for all clients.

  • Relational databases (like PostgreSQL) are a good fit to store relations and well-defined constrained data. Document databases (like MongoDB) are great for dynamic data structures.

  • You should utilize the powerful data integrity constraints and schema validators that are natively offered by database services.