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

Designing a GraphQL schema

This chapter covers

  • Planning UI features and mapping them to API operations

  • Coming up with a schema language text based on planned supported operations

  • Mapping API features to sources of data

In the first part of this book, you learned the fundamentals of the GraphQL "language" that API consumers can use to ask GraphQL services for data and instruct them to do mutations. It is now time to learn how to create these GraphQL services that can understand that language.

We are going to build a real data API for a real web application. I picked the name AZdev for it, which is short for "A to Z" of developer resources. AZdev will be a searchable library of practical micro-documentations, errors and solutions, and general tips for software developers.

I am not a fan of useless abstract examples that are removed from practical realities. Let’s build something real (and useful).

1. Why AZdev

When software developers are performing their day-to-day tasks, they often need to look up one particular thing. For example, how to compute the sum of an array of numbers in JavaScript. They are not really interested in scanning through pages of documentations to find the simple code example they need. This is why at AZdev they’ll find an entry on "Compute the sum of the numbers in a JavaScript array" featuring multiple approaches on just that particular code development need.

AZdev is not a question-answer site. It is a library of what developers usually need to look up. It’s a quick way for them to find concise approaches to handle exactly what they need at the moment.

Here are some examples of entries I’d imagine finding on AZdev:

  • Get rid of only the unstaged changes since the last Git commit

  • Create a secure one-way hash for a text value (like a password) in Node

  • The syntax for a switch statement in LanguageX

  • A Linux command to lowercase names of all files in a directory

You can certainly find approaches to these needs by reading documentations and stack-overflow questions, but wouldn’t it be nice to have a site that features specific needs like these with their approaches right there without all the noise?

AZdev is something I wished existed for as long as I remember. Let’s take the first step into making it happen. Let’s build an API for it!

2. The API requirements for AZdev

A great way to start thinking about your GraphQL schema is to look at it from the point view of the UIs you’ll be building and what data operations they’ll require. However, before we do that, let’s first figure out the sources of data the API service is going to use. GraphQL is not a data storage service. It’s just an interface to one (or many).

ch04 fig 01 gqlia
Figure 4. 1. An API server interfaces data-storage services

To make things interesting for the AZdev GraphQL API, we will make it work with two different data services. We’ll use a relational database service to store transactional data and a document database service to store dynamic data. A GraphQL schema can resolve data from many services, even in the same query!

It’s not at all unusual to have many sources of data in the same project. They don’t all have to be database services. A project can make use of a key-value cache service, get some data from other APIs, or even read data directly from files in the file system. A GraphQL schema can interface as many services as needed.

For the API vocabulary, let’s name the model to represent a single micro-documentation entry at AZdev as "Need" and name a single way or method on how that Need can be met as "Approach". A Need can have multiple Approaches. An Approach belongs to a Need.

From now on, I will use the capitalized version of the words "need" and "approach" to refer to the Database/API entities. "We need a Needs table and we’ll approach the Approaches table with some constraints in mind."

I am using simple names for the entities of this small API but when you’re designing a big GraphQL API with entities that are related to multiple other entities you should invest a little bit of time when it comes to naming things.

This is a general programming advice but for GraphQL schemas try to use specific names for your types when possible. For example, if you have a lesson that belongs to a course, name that type "CourseLesson" instead of just "Lesson", even if the database model was named "lessons". This is especially important if your GraphQL service is public. Your database schemas will evolve and the types you use to describe their entities will need to evolve as well. You’ll need to deprecate types and introduce new ones. Specificity makes all that a bit easier.

Anyone can browse AZdev and find Needs and Approaches. Logged-in users can add new Needs and Approaches and they can also up-vote or down-vote Approaches.

  1. AZdev’s main entries for both Needs and Approaches will be stored in a relational database. I picked PostgreSQL for that. We’ll also store the "User" records in PostgreSQL. A relational database like PostgreSQL is great for, well, relations! A Need has many Approaches and is defined by a User.

    Why PostgreSQL: PostgreSQL is a scalable open-source object-relational database that’s free to use and easy to setup. It offers an impressive set of advanced features that will be handy when you need them. It is one of the most popular choices among open-source relational databases.

  2. Extra data elements on Approaches like explanations, warnings, or general notes will be stored in a document-oriented database. I picked MongoDB for that. A document-oriented database like MongoDB is "schemaless", which makes it a good fit for this type of "dynamic" data. An Approach might have a warning or an explanation associated with it, and it might have other data elements that we’re not aware of at the moment. Maybe at some point we’ll decide to add some performance metrics on Approaches or add a list of related links to them. We don’t need to modify a database "schema" for that. We can just instruct MongoDB to store these new data elements.

    Why MongoDB: MongoDB is the most popular open-source document-oriented (NOSQL) database. The company behind it (MongoDB Inc) offers a community edition that is free to use and available on the 3 major operating systems.

Please note that in this book we will not be covering the installation of PostgreSQL or MongoDB. If you want to follow along with what we’ll do in the book (and you should!) please pause here and go install both services locally in your operating system or sign-up for "hosted" alternatives to use these services.

Please also note that this book will not be a proper source for you to learn these database services. However, the concepts we are going to use in these services will be briefly explained and all the commands and queries related to them will be provided as we progress through the API. I’ll also provide some seed dev data to help us practically test the API features we add.

All the commands in this book will be for Linux. They will also work on a Mac machine because macOS is a Linux-based OS. On Windows, you’ll have to find the native equivalent of these commands or you can spare yourself a lot of trouble and use the "Windows subsystem for Linux" (see az.dev/wsl). If that is not an option, you can also run a Linux machine in a virtual hardware environment like VirtualBox.

If developing natively on Windows is your only option, I recommend using PowerShell instead of the CMD command. Most of Linux Bash shell commands work with PowerShell.

We will be using Node.js in this book and in general, Windows is not the best option when it comes to working with Node-based applications. Node was originally designed for Linux and many of its internal optimizations depend on the native APIs of Linux. Windows-support for Node started a few years after Node was first released and there are active efforts into making it "better" but it will never be as good as Node for Linux.

Running Node on Windows natively is still an option but it’s just one that’ll give you some troubles. Only develop natively on Windows if you plan to actually host your production Node applications on Windows servers.

2.1. The core types

The main entities in the API I’m envisioning for AZdev are User, Need, and Approach. These will be represented with database tables in PostgreSQL. Let’s make each table have a unique "identity" column and an automated "creation" date-time column.

In a GraphQL schema, the tables are usually mapped to "object types" and their columns are mapped to "fields" under these object types. I’ll use the term "model" to represent an entity in the API (a table represented by a GraphQL type) and I’ll use the term field from now on to represent a property on an object under that model. Models are usually defined with PascalCase while fields are defined with camelCase.

The User model will have fields to represent the information of a user, let’s start with "name" and email".

Both the Need and Approach models will have a "content" field to hold their main text content. Here’s a schema language definition (SDL) text that can be used to define the 3 core models in this API with their fields so far:

Listing 4. 1. SDL for the 3 core output object types
type User {
  id: ID!
  createdAt: String!
  email: String
  name: String

  # More fields for a User object
}

type Need {
  id: ID!
  createdAt: String!
  content: String!

  # More fields for a Need object
}

type Approach {
  id: ID!
  createdAt: String!
  content: String!

  # More fields for an Approach object
}

Open your code editor and type in the initial schema text in listing 4.1 and modify it as we progress through the analysis of the AZdev API. The rest of SDL listings in this chapter will omit some existing parts of the schema for clarity.

Note how I defined the id field using the ID type. The ID type is a special one that means a unique identifier and it gets serialized as a String (even if the resolved values for it were integers). Using strings for IDs (instead of integers) is usually a good practice in JavaScript applications. The integer representation in JavaScript is limited.

Also note how I defined the createdAt "date" field as a String. GraphQL does not have a built-in format for date-time fields. The easiest way to work with these fields is to serialize them as string in a standard format (like ISO/UTC).

The exclamation mark after the ID and String types indicate that these fields cannot have null values. Each record in the database will always have an id value and a createdAt value. This exclamation mark is known as a "type modifier" because it modifies a type like String to be not null.

Another type modifier is a pair of square brackets around the type (for example, [String]) to indicate that a type is a list of items of another type. These modifiers can also be combined. For example, [String!]! means a non-null list consisting of non-null string items. We’ll see an example of that shortly.

Note that if a field is defined with the type [String!]! that does not mean the response of that field can’t be an "empty" array. It means it can’t be null.

If you want a field to always have a non-empty array, you can add that validation in its resolver’s logic or define a custom GraphQL type for it.

The id and createdAt fields are examples of how your GraphQL schema types don’t have to exactly match the column types in your database. GraphQL gives you flexibility into casting one type from the database into a more useful type for the client. Try to spot other examples of this as we progress through the API.

The object types in listing 4.1 are known as "output types" as they are used as output to operations, and also to distinguish them from "input types" which are often used as input to mutation operations. We’ll see examples of input types later in this chapter.

Emails and password

I made the email field nullable although I plan on using the email field as a "username" in the application, which means a user’s email cannot be null. So why exactly did I make the email field nullable?

Also, why is there no password field in the User type?

It’s important to remember here that we are not designing a database schema (yet), we are designing an API schema. Some GraphQL tools will let you generate a database schema from a GraphQL API schema but that limits the important differences these two have.

The biggest example is the password field, which should never be a readable part of the API schema. It will however be part of the database schema (which we’re building in the next chapter).

Note that the password field will be part of the mutations to create or authenticate a user as we’ll see later in this chapter.

Also, the email field will be not null in the database schema but in the API, a user’s email is a private and should not be exposed at all unless the user asking for it has some sort of admin access. This means we have to make it part of the API schema but still allow the server to resolve it as null if the user requesting it does not have the right access for it.

This is a good start for the 3 core object types. We’ll add more fields to them as we discuss their relation to UI features. Let’s do that next.

3. Queries

I like to come up with pseudo-code-style operations (queries, mutations, and subscriptions) that are based on the envisioned UI context and then design the schema types and fields which can support these operations.

Let’s start with the queries.

3.1. List of latest Need records

On the main page of AZdev, I’d like to have a list of the latest Need records. The GraphQL API has to provide a query root field for that. This list will be limited to just the last 100 records and it will always be sorted by the creation timpestamp (newer first). Let’s name this field needMainList:

Listing 4. 2. Pseudo Query #2: needMainList
query {
  needMainList {
    id
    content

    # Fields on a Need object
  }
}

A query root field is one that is defined under the Query type directly. Every GraphQL schema starts with its root fields. They are the entry points with which API consumers will always start their data queries. These fields are also known as "top-level" fields.

Note how I named the root field needMainList instead of a more natural name like mainNeedList. This is just a style preference but it has a good advantage. By putting the subject of the action (need in this case) first, all actions on that subject will naturally be grouped together alphabetically in file trees and API explorers. This is helpful in many places but you can think about the auto-complete list in GraphiQL for an example. If you’re looking for what you can do on a Need model, you just type "need" and all relevant actions will be presented in order in the auto-complete list. I’ll follow this practice for all queries, mutations, and subscriptions on every entity of this API.

To support the simple needMainList query root field, here’s a possible schema design for it:

Listing 4. 3. Incremental UI-driven schema design
type Query {
  needMainList: [Need!]

  # More query root fields
}

The type for the new needMainList is [Need!]. The square brackets modify the type to indicate that this field is a list of objects from the Need model. The resolver of this field will have to resolve it with an array. The exclamation mark after the Need type inside the square brackets indicates that all items in this array should have a value and that they cannot be null as well.

Another way to implement the main needs list is to have a generic needList root field and make that field support arguments to indicate any desired sorting and what limit to use. This is actually a more flexible option as it can be made to support many specific lists. However, flexibility comes with costs. When designing a public API, it’s safer to implement the exact features of the currently envisioned UI and optimize and extend the API according to the changing demands of the UI. Specificity helps in making better changes going forward.

Root fields nullability

One general good practice in GraphQL schemas is to make the types of fields non-null if possible unless you have a reason to distinguish between null and empty. A non-null type can still hold an empty value. For example, a non-null string can be an empty one, a non-null list can be an empty array, and even a nun-null object can be one with no properties.

Only use nullable fields if you want to associate an actual semantic meaning to the absence of their values. However, root fields are special because making them nullable has an important consequence. In GraphQL.js implementations, when an error is thrown in any field’s resolver, the built-in executor will resolve that field with null. When an error is thrown in a resolver for a field that is defined as non-null, the executer will propagate the nullability to the field’s parent instead. If that parent field is also non-null, it’ll continue up the tree until it finds a nullable field.

This means, if the root needMainList field were to be made non-null, when an error is thrown in its resolver, the nullability propagates to the Query type itself (its parent). So the whole data response for a query asking for this field will be null, even if the query has other root fields.

This is not ideal. One bad root field should not block the data response of other root fields. When we start implementing this GraphQL API in the next chapter, we’ll see an example of that.

This is why I made the needMainList nullable and it’s why I will make ALL root fields nullable. The semantic meaning to this nullability would in this case be "something went wrong in the resolver of this root field, and we’re allowing it so that a response can still have partial data for other root fields".

3.2. Search and the union/interface types

The main feature of the AZdev UI is its search form. Users will use that to find both Need and Approach objects.

ch04 fig 02 gqlia
Figure 4. 2. Mock of AZdev main landing page

To support a search form, the GraphQL API should provide a query root field for it. Let’s name that field search.

The GraphQL type of this search root field is going to be an interesting one. It will have to perform a "full text" SQL query to find records and sort them by "relevance". Furthermore, that SQL query will have to work with 2 models and return a "mixed" list of matching Need and Approach object which might have different fields!

For example, in the UI, when the search result item is a Need record, let’s make it display how many Approach records it has and when it is an Approach record, let’s make it display the Need information for that Approach record.

To support that, we can simply add these new fields to the Need and Approach types:

Listing 4. 4. The approachCount and need fields
type Need {
  # ·-·-·
  approachCount: Int!
}

type Approach {
  # ·-·-·
  need: Need!
}

However, the search root field can’t be a list of Need records or a list of Approach records. It has to group these 2 models under a new type. In GraphQL, you can model this "grouping" with either a Union type or an Interface type. I’ll tell you how to implement both of these types but first, let’s understand WHY exactly do we need to group these 2 models in one list? Why not do something like:

Listing 4. 5. A simple query for the search field
query {
  search(term: "something") {
    needList {
      id
      content
      approachCount
    }
    approachList {
      id
      content
      need {
        id
        content
      }
    }
  }
}

This simple design works okay but has one major problem. It returns 2 different lists for search results. That means we cannot have an accurate "rank" of search result based on relevance. We can only rank them per set.

To improve this design, we have to return one list of all the objects matching the search term so that we can rank them based on relevance. However, since these objects can have different fields, we would need to come up with a new type that combines them.

One approach to do that is to make the search root field represent an array of objects that can have nullable fields based on what model they belong to. For example, something like this:

Listing 4. 6. A better query for the search field
search(term: "something") {
  id
  content

  approachCount // when result is a Need

  need {        // when result is an Approach
    id
    content
  }
}

This is better and it solves the rank problem. However, it’s a bit messy as the API consumer will have to rely on what fields are null to determine how to render them.

GraphQL offers better solutions for this exact challenge. We can group search result items under either a Union type or a GraphQL type.

3.2.1. Using a union type:

Remember that a union type represents an "OR" logic (as we’ve discussed in Chapter 3). A search result can be either a Need OR an Approach. We can use the introspective __typename to ask the server what type a search result item is and we can use inline fragments to conditionally pick the exact fields our UI requires based on the type of the returned item (just like we did for GitHub’s issueOrPullRequest example in Chapter 3).

With a Union implementation, this would be the query a consumer can use to implement the search feature:

Listing 4. 7. Querying a union type
query {
  search(term: "something") {
    type: __typename
    ... on Need {
      id
      content
      approachCount
    }
    ... on Approach {
      id
      content
      need {
        id
        content
      }
    }
  }
}

Note how this query is just a more structured version of the query in listing 4.6. The "when x is y" comment we had there is now an official part of the query, thanks to inline fragments and the consumer knows exactly what type an item is thanks to the __typename introspective field.

In the GraphQL schema language, to implement this union type for the search root field, we use the union keyword with the pipe character (|) to form a new object type:

Listing 4. 8. Implementing search with a union type
union NeedOrApproach = Need | Approach

type Query {
  # ·-·-·
  search(term: String!): [NeedOrApproach!]
}

Note how I added parenthesis after the search field to indicate that this field will receive an argument (the search term). Also note how a search result will also always be an array and any items in that array cannot be null as well. It can however be an empty array (when there are no matches).

3.3. Using an interface type:

The query in listing 4.7 has a bit of duplication. Both the id and content fields are "shared" between the Need and Approach models. In chapter 3, we saw how shared fields can be implemented using a GraphQL interface type.

Basically, we can think of a search item as an object that has 3 main properties (type, id, and content). This is its main "interface". It can also have either an approachCount or a need fields (depending on its type).

This means we can write a query to consume the search root fields as this:

Listing 4. 9. Querying an interface type
query {
  search(term: "something") {
    type: __typename
    id
    content
    ... on Need {
      approachCount
    }
    ... on Approach {
      need {
        id
        content
      }
    }
  }
}

There are no duplicated fields in this version and that’s certainly a bit better but how exactly do we decide when to pick an interface over union or the other way around?

I ask this question: Are the models (to be grouped) similar but have a few different fields or are they completely different with no shared fields?

If they have shared fields then an interface is a better fit. Only use unions with the grouped models have no shared fields.

In the GraphQL schema language, to implement an interface type for the search root field, we use the interface keyword to define a new object type that defines the shared fields and make all the models (to be grouped) "implement" the new interface type (using the implements keyword):

Listing 4. 10. Implementing search with an interface type
interface SearchResultItem {
  id: ID!
  content: String!
}

type Need implements SearchResultItem {
  # ·-·-·
}

type Approach implements SearchResultItem {
  # ·-·-·
}

type Query {
  # ·-·-·
  search(term: String!): [SearchResultItem!]
}

Besides the fact that the consumer query is simpler with an interface, there is a subtle reason why I prefer the interface type here. With the interface type, looking at the implementation of the Need/Approach types, you can easily tell they are part of another type, while with unions, you can’t. You have to find what other types use them by looking at code elsewhere.

A graphql type can also implement multiple interface types. In SDL, you can just use a comma-separated list of interface types to implement.

To make Need objects more discoverable in search, let’s also enable API users to supply a list of "tags" in their search query. A tag can be something like "git", "javascript", "command", "code", etc.

This means in the database, we need to store tags on each Need object, and let’s also make them part of the data response for each field that returns Need objects. We’ll have to add a tags field under the Node model. It can simply be an array of strings:

Listing 4. 11. The tags field
type Need implements SearchResultItem {
  # ·-·-·
  tags: [String!]!
}

To support including a list of tags in the search query, we need to make it part of the search field’s arguments:

Listing 4. 12. The tags field
type Query {
  # ·-·-·
  search(
    term: String
    tags: [String!]
  ): [SearchResultItem!]
}

Note how I made both arguments nullable to allow the search to be used to browse one or more tags without a search term.

3.4. The page for one Need record

Users of the AZdev UI can select a Need entry on the home page (or from search results) to navigate to a page that represents a single Need record. That page will have the record’s full information including its list of Approaches.

The GraphQL API will have to provide a query root field to enable consumers to get data about one Need object. Let’s name this root field "needInfo".

Listing 4. 13. Pseudo Query #3: needInfo
query {
  needInfo (
    # Arguments to identify a Need record
  ) {
    # Fields under a Need record
  }
}

To identify a single Need record, we can make this field accept an id argument. Here is what we need to add in the schema text to support this new root field:

Listing 4. 14. Incremental UI-driven schema design
type Query {
  # ·-·-·
  needInfo(id: ID!): Need
}

Great. This enables API users to fetch full information about a Need object but how about the information of the Approach objects that are defined under that Need object? How do we enable them to fetch these?

Also, because users of the AZdev application will be able to vote on Approaches, we should probably make the API return the list of Approaches sort by their number of votes. The simplest way to account for the number of votes on Approaches is to just add a "cache" of how many current votes each Approach object has. Let’s do that:

Listing 4. 15. The voteCount field
type Approach implements SearchResultItem {
  # ·-·-·
  voteCount: Int!
}

Now, to return the list of Approaches that are "related" to a Need object, we need to talk about entity relationships.

3.5. Entity relationships

The "list of Approaches" under a Need object represents a "relationship". A Need object can have many Approach objects.

There are a few other relationships that we have to support as well:

  • When displaying a Need record in the UI, we should display the name (or username) of the user who created it. The same applies to Approach objects as well.

  • For each Approach, the application will display its list of extra detail data elements. It’s probably a good idea to have a new "Approach Detail" object type to represent that relation.

These are 4 relationships that we need to represent in this API:

  • A Need has many approaches

  • A Need belongs to a user

  • An Approach belongs to a user

  • An Approach has many detail records

In the database, these relations are usually represented with integers in identity columns (primary keys and foreign keys). The clients of this API are really interested in the data these ids represent. For example, the name of the person who authored a Need record or the content of the approaches defined on it. That’s why a client is expected to supply a list of leaf fields when they include these relation fields in a GraphQL query.

Listing 4. 16. Relation fields under needInfo
query {
  needInfo (
    # Arguments to identify a Need record
  ) {
    # Fields under a Need record

    author {
      # Fields under a User record
    }

    approachList {
      # Fields under an Approach record

      author {
        # Fields under a User record
      }

      detailList {
        # Fields under an Approach Detail record
      }
    }
  }
}

Note how I named the field representing the "user" relationship as "author". I’ve also named the list of detail records detailList instead of approachDetailList. The name of a field does not have to match the name of its type or database source.

To support these relationships in the schema, we add references to their core types:

Listing 4. 17. Incremental UI-driven schema design
type ApproachDetail {
  content: String!

  # More fields for an Approach Detail record
}

type Approach implements SearchResultItem {
  # ·-·-·
  author: User!
  detailList: [ApproachDetail!]!
}

type Need implements SearchResultItem {
  # ·-·-·
  author: User!
  approachList: [Approach!]!
}

Note how I used the User type for the author field. We’ve also planned the to use the same User type under the me field scope. This introduces a problem because of the needList field. When a user is asking for their own Need records, that’ll work fine. However, when the API reports the author details of a public Need record, these details should not include the needList of that author. Can you think of a solution to this problem? We’ll figure this out as we implement the me field scope (in Chapter 7).

3.6. The ENUM type

An Approach Detail record is just a text field (which I named content) but it is a special one because it’ll be under a certain category. The initial set of categories that we’d like the API to support are NOTE, EXPLANATION, and WARNING. Since these 3 categories will be the only accepted values in an Approach Detail’s categories, we can use GraphQL’s special ENUM type to represent them. Here is how to do that (in SDL):

Listing 4. 18. The ApproachDetail ENUM type
enum ApproachDetailCategory {
  NOTE
  EXPLANATION
  WARNING
}

The special ENUM type allows us to enumerate all the possible values for a field. This adds a layer of validation around that field making the enumerated values the only possible ones. This is especially helpful if you’re accepting input from the user for an enumerated field but it’s also a good way to communicate through the type system that a field will always be one of a fixed set of values.

Now we can modify the ApproachDetail GraphQL type to use this new ENUM type:

Listing 4. 19. GraphQL type for an Approach Detail
type ApproachDetail {
  content: String!
  category: ApproachDetailCategory!
}

This is a good start for the GraphQL types we need to support the queries we plan for the UI. It is however just a start. There are more fields to think about for all core types and we’ll possibly need to introduce more types as we make progress on the API.

At some point, we need to think about offering the option for API consumers to "paginate" through list-type fields like needMainList, search, approachList and detailList (under needInfo), needList (under me). We should not have the API return all the records under a list.

3.7. The page for a user’s own Need records

Let’s give logged-in users the ability to see the list of their own Need records. Let’s name the field to support that needList.

However, making the needList field a root one might be confusing. It could be interpreted as a field to return a list of all Need records in the database!

We can name it differently to clear that confusion but there is another useful practice to follow here that can also solve this confusion. We can introduce a query root field that represent the scope of the "currently logged-in user" and put the needList field under it.

This field is commonly named me in GraphQL APIs but that name is not a requirement. You can name it anything.

Listing 4. 20. Pseudo Query #4: The list of Need records for an authenticated user
query {
  me (
    # Arguments to validate user access
  ) {
    needList {
      # Fields under a Need record
    }
  }
}

Any fields under the me field will be filtered for the currently logged-in user. In the future, we might think of adding more fields under that scope. The me field is a good way to organize multiple fields related to the current user.

To support me { needList } feature, we will have to introduce 2 fields in the schema: A root me field that returns a User type and a needList field on the User type:

Listing 4. 21. Incremental UI-driven schema design
type User {
  # ·-·-·
  needList: [Need!]!
}

type Query {
  # ·-·-·
  me: User
}

Once again, note how I made the me field nullable. A session might time-out on the backend but instead of returning a completely null response for a query that has a me field, we can return null for the timed-out me field but still include partial data in other parts of the query.

Great. A logged-in user can now ask for their own Need records but how exactly will a user login to the API and how do we determine if a query request is by a logged-in user? This is a good place to talk about the concept of authentication (and authorization) in GraphQL

3.8. Authentication and authorization

The me field is going to require an "authentication" token to be sent with any query that includes it. That token will be used to identify the user who is making the call.

The authToken is similar to the concept of a session "cookie". We’ll need a way to generate these authentication token values for users after they login to the AZdev application. We’ll figure that out later in this chapter.

Authorization is the business logic that determines whether a user has permission to read a piece of data or perform an action. For example, an authorization rule in this API could be:

"Only the owner of a Need record can delete that record"

4. Mutations

To add content to AZdev (Needs, Approaches, Details, Votes), a guest has to create an account and login to the application. This will require the API to host a "users" database table to store users' credentials. Passwords in that table will need to be one-way hashed. The GraphQL API will need to provide mutations to create a user and allow them to obtain an authorization token.

Listing 4. 22. Pseudo Mutation #1: userCreate
mutation {
  userCreate (
    # Input for a new User record
  ) {
    # Fail/Success response
  }
}
Listing 4. 23. Pseudo Mutation #2: userLogin
mutation {
  userLogin (
    # Input to identify a User record
  ) {
    # Fail/Success response
  }
}

The userCreate mutation will enable users to create an account for the AZdev application and the userLogin mutation will enable them to perform future queries and mutations that are specific to them. All user-related operations will have to have some way to identify the user who is requesting them. The simplest way to do that is to use field arguments and make the user’s auth token part of the input for these operations.

Using field arguments to pass an auth token is certainly not the only way. The GitHub GraphQL API requires the clients to include their auth tokens in the HTTP headers of each API request. I’ll use the simple field arguments approach in this book.

Note how for each mutation I am planning to handle a "fail response" as well as the normal success response. Mutations will typically rely on valid user input to succeed. It’s a good idea to represent the errors caused by invalid uses of mutations differently from other root errors a GraphQL API consumer can cause. For example, trying to request a non-existing field is a root error. However, trying to create a user with an email address that’s already in the system is just a "user error" that we should handle differently.

The root errors field is used for server problem (like 5xx HTTP codes) but it is also used for some client problem. For example, hitting the limit on a rate-limited API or accessing something without the proper authorization. GraphQL will also use that field if the client sends a bad request that fails the schema validation. The "payload error" concept is suitable for user-friendly messages when they supply bad input.

Using user-friendly errors in payloads acts as an "error boundary" for the operation (just like error boundary components in React, for example). Some developers even use payloads with errors in query fields as well. You can use them to hide implementation details instead of exposing thrown errors to the consumer directly.

We can implement this fail/success response with either a union type or a special output "payload" type for each entity in the system. I’ll use the payload concept for the mutations of AZdev.

Mutations output payload can include user errors, the entity they operate on, and any other values that might be useful. For example, the userLogin mutation can include the generated auth token value as part of its output payload. Here’s an example of how that can be done:

Listing 4. 24. Incremental UI-driven schema design
type UserError {
  message: String!
}

type UserPayload {
  errors: [UserError!]!
  user: User
  authToken: String
}

# More entity payloads

type Mutation {
  userCreate(
    # Mutation Input
  ): UserPayload!

  userLogin(
    # Mutation Input
  ): UserPayload!

  # More mutations
}

Note how I kept the authToken field separate from the user field in the UserPayload type. I think this makes any use of this API cleaner. The authToken value is not really part of a User record, it’s just a temporary value for them to authenticate themselves for future operations. They will need to renew it at some point.

This takes care of the output of these 2 mutation operations. We still need to figure out the structure of their input.

I kept the UserError type simple with just one required message field. This matches the structure of the GraphQL root errors array. I think it’s a good idea to also support the optional path and locations fields in this type to give API consumers more power into figuring out what to do with these errors.

4.1. Mutations input

Mutations always have some kind of input that usually comprises multiple fields. To better represent and validate the structure of a multi-field input, GraphQL support a special input type that can be used to group scalar input values into one object.

For example, for the userCreate mutation, let’s allow the API client to specify their first name, last name, email, and password. All of these fields are strings.

Instead of defining 4 scalar input arguments for the userCreate mutation, we can group these input values as one input object argument. We use the input keyword for that:

Listing 4. 25. Incremental UI-driven schema design
# Define an input type:
input UserInput {
  email: String!
  password: String!
  firstName: String
  lastName: String
}

# Then use it as the only argument to the mutation:
type Mutation {
  userCreate(
    input: UserInput!
  ): UserPayload!

  # More mutations
}

A couple of things to note about this new type:

  • You can use any name for the input object type and the mutation argument. However, the names <Model>Input and input are the common convention. I’ll use these conventions in the AZdev schema.

  • Having firstName and lastName optional allows a user to register an account with just their email (and password). The database will have to match this as well and have the columns associated with these fields as nullable.

The UserInput type is similar to the core User type we designed for the queries for this API. The core User type actually include the exact same structure of the UserInput type!

So, why introduce a new input object type when we already have the core object type for a user?

Input object types are basically a more simplified version of output object types. Their fields can’t reference output object types (or interface/union types). They can only use scalar input types or other input object types.

Input object types are often smaller and closer to the database schema while object types can be different and are likely to introduce more fields to represent relations or other custom logic. The id field for example is a required part of the User type but we do not need it in the UserInput type because it’s a value that’ll be generated by the database. Some fields will appear in input object types but should not be in the corresponding output object types at all. An example of that is the password field. We need it to create a user account (or login) but we should never expose it in any readable capacity.

While you can pass the email, firstName, and lastName values directly to the mutation, the input object type structure is preferable because it allows passing an object to the mutation. This often reduces the complexity of the code using that mutation and enhances code readability in general. Having an input object also adds a reusability benefit to your code.

Although the benefit of using an input object type is when you have multiple scalar input values, it’s a good practice to use the same pattern across all mutations. Even the ones that have a single scalar input value.

For the userLogin mutation, we need the user to send over their email and password. Let’s create an AuthInput type for that:

Listing 4. 26. Incremental UI-driven schema design
input AuthInput {
  email: String!
  password: String!
}

type Mutation {
  # ·-·-·
  userLogin(
    input: AuthInput!
  ): UserPayload!

  # More mutations
}

4.2. Creating and Updating a Need

To create a new Need record in the AZdev application, let’s make the API support a needCreate mutation.

For all mutations from now on, the API consumer is expected to have a valid access token. Here’s what a mutation to create a Need record might look like:

Listing 4. 27. Pseudo Mutation #3: needCreate
mutation {
  needCreate (
    # Input for a new Need record
  ) {
    # Fail/Success Need payload
  }
}

To update an existing Need record, let’s make the API support a needUpdate mutation. This could be helpful to fix a typo or for future improvements of the content of a Need record.

Listing 4. 28. Pseudo Mutation #4: needUpdate
mutation {
  needUpdate (
    # Input to identify a Need record
    # Input for updated Need record
  ) {
    # Fail/Success Need payload
  }
}

To support these mutations, we need to define the Need input and payload types and create new mutation fields that use them.

For both mutations, the input object’s main field is the simple text field for the content field on a Need record. There is also the tags field which is an array of string values. Let’s also enable users to create private Needs that are not to be included in the search (unless the user who is searching owns them).

Private Need entries will be handy for users to keep a reference of things they need in their private projects. Keep in mind that they will make things a bit more challenging since we need to exclude them unless the API consumer is the user who owns them.

Here’s the SDL that represent what we planned for the Need entity mutations:

Listing 4. 29. Incremental UI-driven schema design
input NeedInput {
  content: String!
  tags: [String!]!
  private: Boolean!
}

type NeedPayload {
  errors: [UserError!]!
  need: Need
}

type Mutation {
  # ·-·-·

  needCreate(
    input: NeedInput!
  ): NeedPayload!

  needUpdate(
    needId: ID!          (1)
    input: NeedInput!
  ): NeedPayload!
}
1 Because this is an update operation, we need a way to lookup the record to be updated. Keeping this needId argument separate from the input argument allowed us to reuse the same NeedInput type.

4.3. Creating and voting on Approach entries

To create a new Approach record on an existing Need record, let’s make the API support an approachCreate mutation:

Listing 4. 30. Pseudo Mutation #5: approachCreate
mutation {
  approachCreate (
    # Input to identify a Need record
    # Input for a new Approach record (with ApproachDetail)
  ) {
    # Fail/Success Approach payload
  }
}

A logged-in user viewing a Need record page along with the list of its mutations can upvote or downvote a single Approach record. Let’s make the API support an approachVote mutation for that. This mutation needs to return the new votes count on the voted approach. We’ll make that part of the Approach payload.

Listing 4. 31. Pseudo Mutation #6: approachVote
mutation  {
  approachVote (
    # Input to identify an Approach record
    # Input for "Vote"
  ) {
    # Fail/Success Approach payload
  }
}

Here are the schema text changes needed to support these 2 new mutations:

Listing 4. 32. Incremental UI-driven schema design
input ApproachDetailInput {
  content: String!
  category: ApproachDetailCategory!  (1)
}

input ApproachInput {
  content: String!
  detailList: [ApproachDetailInput!]!
}

input ApproactVoteInput {
  up: Boolean!
}

type ApproachPayload {
  errors: [UserError!]!
  approach: Approach
}

type Mutation {
  # ·-·-·

  approachCreate(
    authToken: String!
    needId: ID!
    input: ApproachInput!
  ): ApproachPayload!

  approachVote(
    authToken: String!
    approachId: ID!
    input: ApproachVoteInput!
  ): ApproachPayload!
}
1 The ENUM type here will validate the accepted categories

Note how I opted to represent up-votes and down-votes with a simple Boolean field and not an ENUM of 2 values. That’s an option when the set of accepted values are only 2. It’s probably better to use an ENUM for this anyway but let’s keep it as a Boolean and add a "comment" to clarify it. This is a good place to introduce that concept. We just put the clarification text on the line before the field that needs it and surround that text with triple-quotes ("""):

Listing 4. 33. Adding a description text
input ApproactVoteInput {
  """true for up-vote and false for down-vote"""
  up: Boolean!
}

This clarifying text is known as "description" in a GraphQL schema and it’ll be part of the actual structure of that schema. It’s not really a comment but rather a property of this type. Tools like GraphiQL expect it and display it in auto-complete lists and documentation explorers. You should consider adding a description property to any field that could use an explanation. We’ll see more examples of this in the next chapter.

We should probably also support a userUpdate mutation and an approachUpdate one. I’ll leave that as a multi-chapter exercise for you. In this chapter, you need to plan for how these mutations will be called and come up with the SDL text for them.

Note again how I am naming all the mutations using the form <model>Action (e.g. needCreate) rather than the more natural action<Model> (e.g. createNeed). Now all actions on a Need record are alphabetically grouped together. We’ll find the needMainList, needInfo, needCreate, needUpdate operations all next to each other.

Note how the ApproachDetailInput type (listing 4.26) is identical to the ApproachDetail type (listing 4.14). However, don’t be tempted to reuse output object types as input object types. In the future, we might upgrade the Approach Detail concept to also have unique IDs and creation time-stamps. There is also a great value in keeping everything consistent.

5. Subscriptions

On Twitter and other social media apps, while you’re looking at a post, its replies/shares/likes counters get auto-updated. Let’s plan for a similar feature for the votes counts. While looking at the list of Approaches in the Need page, let’s make their votes auto-updated!

We can use a subscription operation for that. This operation will have to accept a needId input so that a user can subscribe to the vote changes on Approaches under a single Need object (rather than all Approaches in the system).

Listing 4. 34. Pseudo Subscription #1: voteChanged
subscription {
  voteChanged (
    # Input to identify a Need record
  ) {
    # Fields under an Approach record
  }
}

On the AZdev home page which shows the list of all the latest Needs, another subscriptions-based feature that would add a good value there is to show an indicator telling the user that there are new Need records avaialble. They can click that indicator to show them. Let’s name this one needMainListChanged:

Listing 4. 35. Pseudo Subscription #2: needLatest
subscription {
  needMainListChanged {
    # Fields under a Need record
  }
}

To support these subscriptions, we define a new Subscription type with the new fields under it like this:

type Subscription {
  voteChanged(needId: ID!): Approach!
  needMainListChanged: [Need!]
}

How does all that sound to you? Let’s make it happen!

I will be adding more features to the AZdev API after this book but we need to keep things simple and manageable here. You can explore the AZdev current production GraphQL API at az.dev/api and see what other queries, mutations, and subscriptions I ended up adding to AZdev.

6. Full schema text

Did you notice how I came up with the whole schema description so far just by thinking in terms of the UI. How cool is that? You can give this simple schema language text to the front-end developers on your team and they can start building the front-end app right away! They don’t need to wait for your server implementation. They can even use some of the great tools out there to have a mock GraphQL server that resolves these types with random test data.

The schema is often compared to a contract. You always start with a contract.

The full schema text representing the initial version of the AZdev GraphQL schema with the types can be found at az.dev/gia

With this schema text, we can start the efforts of building the frontend application for AZdev right away. However, in this book we are not going to build the frontend application. We’re just implementing the API for it. Let’s start that process in the next chapter.

I’ll repeat relevant sections of this schema text when we work through the tasks of implementing them.

7. Summary

  • An API server is an interface to one or many data sources. GraphQL is not a storage engine, it’s just a runtime that can power an API server.

  • An API server can talk to many "types" of data services. Data can be queried from databases, cache services, other APIs, files, etc.

  • A good first step to design a GraphQL API is to draft a list of operations that will theoretically satisfy the needs of the application that you’re planning. Operations include queries, mutations, and subscriptions.

  • Draft GraphQL operations can be used to design the tables and collections in databases and to come up with the initial GraphQL Schema Language text.