What I Learned About GraphQL While Developing a Shopify App (Part 2)

Agustin Queirolo
.
March 8, 2023
What I Learned About GraphQL While Developing a Shopify App (Part 2)

In part 1 we discussed what exactly GraphQL is and how it was used in our project to send requests to Shopify’s GraphQL server. Our team quickly realized that we needed to implement an internal server for our Shopify app as well, to handle requests, authorization, database operations and so on.

Since the Node.js boilerplate code provided by Shopify we mentioned in part 1 includes a small GraphQL server, it made sense to use that one as a base and adapt it to our needs. A GraphQL server requires a different set of tools and libraries compared to a REST server. Our work consisted of understanding what technologies and concepts are used to implement this type of server, how the Shopify boilerplate server works and how we can change it to add new functionalities. In this last part of our blog we will go through all this in the hope you will find it useful as well.

Technologies and concepts

Apollo

The code that Shopify provided already comes with a server built with Apollo, a set of libraries for working with GraphQL. Apollo makes it easier to build and maintain GraphQL servers by providing a unified API and a set of best practices for implementing GraphQL. With Apollo, developers can focus on writing the logic for their APIs, while Apollo handles the details of handling GraphQL requests and responses. In this post we are going to talk about Apollo Server, its library for implementing GraphQL servers.

Schema

Back in part 1 we mentioned how a schema defines the structure of the data that can be queried on a GraphQL server. Here’s a short example schema that defines two object types: Book and Author (courtesy of the Apollo documentation):

no-line-numbers|graphql type Book { title: String author: Author } type Author { name: String books: [Book] }

This schema defines a collection of types and the relationships between those types. In the example schema above, a Book can have an associated author, and an Author can have a list of books.

The Query type is a special object type that defines all of the top-level entry points for queries that clients execute against your server. Each field of the Query type defines the name and return type of a different entry point. Let’s take a loot at the following example:

no-line-numbers|graphql type Query { books: [Book] authors: [Author] }

This Query type defines two fields: books and authors. Each field returns a list of the corresponding type. With a REST-based API, books and authors would probably be returned by different endpoints (e.g., /api/books and /api/authors). The flexibility of GraphQL enables clients to query both resources with a single request.

There is another type called Mutation. It is similar in structure and purpose to the Query type. Whereas the Query type defines entry points for read operations, the Mutation type defines entry points for write operations.

no-line-numbers|graphql type Mutation { addBook(title: String, author:String): Book }

This Mutation type defines a single available mutation, addBook. The mutation accepts two arguments (title and author) and returns a newly created Book object.

If you wish to learn more about schemas, such as the Subscription type, refer to Apollo Server’s documentation.

Let’s take a look at how the schema was initially defined in Shopify’s boilerplate code.

no-line-numbers|typescript import {gql} from 'apollo-server-express'; export const typeDefs = ` type AdminShop { id: ID! domain: String! publicationId: String! appHandle: String! availableProductCount: Int! onboardingInfoCompleted: Boolean! termsAccepted: Boolean! onboardingCompleted: Boolean! } type Shop { id: ID! country: String! domain: String! name: String! storefrontAccessToken: String! } type Query { adminShop: AdminShop! shop(id: Int!): Shop shops(country: String, nameIsLike: String, reverse: Boolean, domains: [String]): [Shop] shopCountries: [String] } type Mutation { completeOnboardingInfo: AdminShop acceptTerms: AdminShop completeOnboarding: AdminShop } `; export const schema = gql(typeDefs);

This schema defines two object types: AdminShop and Shop, as well as different queries and mutations. If the client wants to retrieve a shop information or change the database to indicate the onboarding process has been completed, this schema provides the necessary entry points. A schema is a very powerful concept and we have only scratched the surface of what you can define on it, yet one thing that you cannot do inside a schema is provide the logic necessary to handle these queries and mutations. We need to implement resolvers for that.

Resolver

A resolver is a function that’s responsible for populating the data for a single field in your schema. We define all resolvers in a single file. Let’s say, for example, that we only want to define a resolver for the shop(id: Int!): Shop query we defined previously. The resulting resolver would look like this:

no-line-numbers|typescript export const resolvers = { Query: { shop: async (_, {id}) => { if (id) { const shop = db.Shop.findOne({ where: { id, }, }); return shop; } return null; }, }, };

The implementation is quite simple, we just use the id we got as a parameter to find a Shop in a database and return the result.

How can we change what Shopify gives us?

Now that we have a basic understanding of how to implement a GraphQL server and we have Shopify’s schema and resolvers available to us, we can start adapting the server to our needs. It is very likely that your server needs will not match 100% with the generic implementation Shopify has so kindly provided and you’ll have to make some changes.

Let’s imagine that you expect two very distinct types of shops to install your Shopify app, shops that sell winter clothes and shops that do not. We want to implement a way to make that distinction. Let’s first go to our schema, to the definition of the type Shop. We are going to include a boolean flag called ‘sellsWinterClothes’ to that type. Since this is a very important field, it makes sense to include an exclamation mark (!) which means that the field is non-nullable. So our type Shop would now look like this:

no-line-numbers|graphql type Shop { id: ID! country: String! domain: String! name: String! storefrontAccessToken: String! sellsWinterClothes: Boolean! }

Now, every time the client uses the queries ‘shop’ or ‘shops’ which are already defined, this information will also be returned. The only thing the client needs to do is specify they want to retrieve the ‘sellsWinterClothes’ field in their query, as discussed in part 1 of this blog post. If no change is made on the client side, they will not receive the new field but no breaking change will happen. They can just continue as usual. That is one of the benefits of GraphQL we mentioned in part 1 of the blog.

If we take a look at the resolver for the shop query, not a single line of code needs to change. We only need to make sure that the DB returns a ‘sellsWinterClothes’ attribute when we query the Shop table. If we intended to define a new query or mutation (for example changing that boolean field) in our schema, we would have to add a resolver for it.

Summing up, if we wish to make changes to a GraphQL server first we need to go to the schema. There, we can create or change an object type, add queries or mutations (or even more things which we haven’t discussed here). Afterwards, changes must be made in the resolvers file accordingly.

Conclusion

In the second part of this blog post we have dived into how we can build a GraphQL server. We have looked at some of GraphQL’s core concepts such as schemas and resolvers. Lastly, we have discussed how to make the best out of the Node.js server Shopify has provided us with and how to adapt it to our specific needs.

GraphQL offers a more efficient and flexible way to request and receive data from APIs, compared to traditional REST. Its ability to handle multiple queries in a single request and its user-friendly query language are noteworthy features. Even though it may seem intimidating at first, it is a great tool which will enable you to make the best out of your Shopify app.

Stay ahead of the curve on the latest trends and insights in big data, machine learning and artificial intelligence. Don't miss out and subscribe to our newsletter!

Download your e-book today!

Download your report today!

In part 1 we discussed what exactly GraphQL is and how it was used in our project to send requests to Shopify’s GraphQL server. Our team quickly realized that we needed to implement an internal server for our Shopify app as well, to handle requests, authorization, database operations and so on.

Since the Node.js boilerplate code provided by Shopify we mentioned in part 1 includes a small GraphQL server, it made sense to use that one as a base and adapt it to our needs. A GraphQL server requires a different set of tools and libraries compared to a REST server. Our work consisted of understanding what technologies and concepts are used to implement this type of server, how the Shopify boilerplate server works and how we can change it to add new functionalities. In this last part of our blog we will go through all this in the hope you will find it useful as well.

Technologies and concepts

Apollo

The code that Shopify provided already comes with a server built with Apollo, a set of libraries for working with GraphQL. Apollo makes it easier to build and maintain GraphQL servers by providing a unified API and a set of best practices for implementing GraphQL. With Apollo, developers can focus on writing the logic for their APIs, while Apollo handles the details of handling GraphQL requests and responses. In this post we are going to talk about Apollo Server, its library for implementing GraphQL servers.

Schema

Back in part 1 we mentioned how a schema defines the structure of the data that can be queried on a GraphQL server. Here’s a short example schema that defines two object types: Book and Author (courtesy of the Apollo documentation):

no-line-numbers|graphql type Book { title: String author: Author } type Author { name: String books: [Book] }

This schema defines a collection of types and the relationships between those types. In the example schema above, a Book can have an associated author, and an Author can have a list of books.

The Query type is a special object type that defines all of the top-level entry points for queries that clients execute against your server. Each field of the Query type defines the name and return type of a different entry point. Let’s take a loot at the following example:

no-line-numbers|graphql type Query { books: [Book] authors: [Author] }

This Query type defines two fields: books and authors. Each field returns a list of the corresponding type. With a REST-based API, books and authors would probably be returned by different endpoints (e.g., /api/books and /api/authors). The flexibility of GraphQL enables clients to query both resources with a single request.

There is another type called Mutation. It is similar in structure and purpose to the Query type. Whereas the Query type defines entry points for read operations, the Mutation type defines entry points for write operations.

no-line-numbers|graphql type Mutation { addBook(title: String, author:String): Book }

This Mutation type defines a single available mutation, addBook. The mutation accepts two arguments (title and author) and returns a newly created Book object.

If you wish to learn more about schemas, such as the Subscription type, refer to Apollo Server’s documentation.

Let’s take a look at how the schema was initially defined in Shopify’s boilerplate code.

no-line-numbers|typescript import {gql} from 'apollo-server-express'; export const typeDefs = ` type AdminShop { id: ID! domain: String! publicationId: String! appHandle: String! availableProductCount: Int! onboardingInfoCompleted: Boolean! termsAccepted: Boolean! onboardingCompleted: Boolean! } type Shop { id: ID! country: String! domain: String! name: String! storefrontAccessToken: String! } type Query { adminShop: AdminShop! shop(id: Int!): Shop shops(country: String, nameIsLike: String, reverse: Boolean, domains: [String]): [Shop] shopCountries: [String] } type Mutation { completeOnboardingInfo: AdminShop acceptTerms: AdminShop completeOnboarding: AdminShop } `; export const schema = gql(typeDefs);

This schema defines two object types: AdminShop and Shop, as well as different queries and mutations. If the client wants to retrieve a shop information or change the database to indicate the onboarding process has been completed, this schema provides the necessary entry points. A schema is a very powerful concept and we have only scratched the surface of what you can define on it, yet one thing that you cannot do inside a schema is provide the logic necessary to handle these queries and mutations. We need to implement resolvers for that.

Resolver

A resolver is a function that’s responsible for populating the data for a single field in your schema. We define all resolvers in a single file. Let’s say, for example, that we only want to define a resolver for the shop(id: Int!): Shop query we defined previously. The resulting resolver would look like this:

no-line-numbers|typescript export const resolvers = { Query: { shop: async (_, {id}) => { if (id) { const shop = db.Shop.findOne({ where: { id, }, }); return shop; } return null; }, }, };

The implementation is quite simple, we just use the id we got as a parameter to find a Shop in a database and return the result.

How can we change what Shopify gives us?

Now that we have a basic understanding of how to implement a GraphQL server and we have Shopify’s schema and resolvers available to us, we can start adapting the server to our needs. It is very likely that your server needs will not match 100% with the generic implementation Shopify has so kindly provided and you’ll have to make some changes.

Let’s imagine that you expect two very distinct types of shops to install your Shopify app, shops that sell winter clothes and shops that do not. We want to implement a way to make that distinction. Let’s first go to our schema, to the definition of the type Shop. We are going to include a boolean flag called ‘sellsWinterClothes’ to that type. Since this is a very important field, it makes sense to include an exclamation mark (!) which means that the field is non-nullable. So our type Shop would now look like this:

no-line-numbers|graphql type Shop { id: ID! country: String! domain: String! name: String! storefrontAccessToken: String! sellsWinterClothes: Boolean! }

Now, every time the client uses the queries ‘shop’ or ‘shops’ which are already defined, this information will also be returned. The only thing the client needs to do is specify they want to retrieve the ‘sellsWinterClothes’ field in their query, as discussed in part 1 of this blog post. If no change is made on the client side, they will not receive the new field but no breaking change will happen. They can just continue as usual. That is one of the benefits of GraphQL we mentioned in part 1 of the blog.

If we take a look at the resolver for the shop query, not a single line of code needs to change. We only need to make sure that the DB returns a ‘sellsWinterClothes’ attribute when we query the Shop table. If we intended to define a new query or mutation (for example changing that boolean field) in our schema, we would have to add a resolver for it.

Summing up, if we wish to make changes to a GraphQL server first we need to go to the schema. There, we can create or change an object type, add queries or mutations (or even more things which we haven’t discussed here). Afterwards, changes must be made in the resolvers file accordingly.

Conclusion

In the second part of this blog post we have dived into how we can build a GraphQL server. We have looked at some of GraphQL’s core concepts such as schemas and resolvers. Lastly, we have discussed how to make the best out of the Node.js server Shopify has provided us with and how to adapt it to our specific needs.

GraphQL offers a more efficient and flexible way to request and receive data from APIs, compared to traditional REST. Its ability to handle multiple queries in a single request and its user-friendly query language are noteworthy features. Even though it may seem intimidating at first, it is a great tool which will enable you to make the best out of your Shopify app.

Stay ahead of the curve on the latest trends and insights in big data, machine learning and artificial intelligence. Don't miss out and subscribe to our newsletter!