Skip to main content

Using the GraphQL API

Installing the GraphQL API

GraphQL provides a query language that allows clients to ask a GraphQL-enabled server for data in whichever shape and structure it needs to consume. The Solidus team officially supports solidus_graphql_api, an extension adding a GraphQL endpoint to the application.

To use the GraphQL extension, you have to add it to the Gemfile:

gem 'solidus_graphql_api'

After that, remember to run:

bundle

Once done, and after (re)starting the Rails server, you'll have a new route POST /graphql ready to answer your GraphQL queries:

curl \
-X POST \
--data '{ "query": "{ currentStore { name } } "}' \
-H "Content-Type: application/json" \
http://localhost:3000/graphql

GraphQL playground

We have a dedicated playground that you can use to learn how to use the GraphQL endpoint on Solidus. It uses the data on Solidus' demo application as the backend.

You can click on the Docs tab on the playground to look at the GraphQL schema and discover all the queries and mutations you can perform. If you prefer, you can check the extension's documentation page instead.

Some of the operations require that you add a header to the request. Look for the Set Headers icon on the playground site.

Demo walkthrough

We're going to walk through a typical flow when interacting with a Solidus storefront. It will help you get familiar with GraphQL on Solidus. You can use our playground as a development platform.

caution

You can use the playground to test all the points except for the Authenticated users section. That's because the demo application uses an on-the-fly user, which is not persisted in the database. For that section, you should use your own application.

For the same reason, understand that the changes you made here won't be visible if you visit http://demo.solidus.io in the browser, and vice-versa. Both requests are associated with different visitors; therefore, no data will be shared between them.

Listing products

One of the core information you need to present to the customers is what products are available to buy. As you might have a lot of them, probably it's a good idea to paginate them. Solidus GraphQL uses cursor-based pagination, where each item gets a unique cursor that you use to get following or previous records. For instance, when implementing forward pagination for products, your first query would look something like the following:

query listProducts {
products(first: 5) {
nodes {
name
slug
masterVariant {
defaultPrice {
amount
currency {
htmlEntity
}
}
images {
nodes {
smallUrl
}
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}

Notice the first variable that is being given to the query. It limits the number of records to be fetched to 5. Also, please pay attention to the endCursor and hasNextPage fields within pageInfo as we're going to use them shortly.

Now, take a look at the Response tab. Notice that NQ value for pageInfo -> endCursor. It uniquely identifies the last received product, i.e., Solidus Hoodie Zip. On the other hand, hasNextPageis telling us that we're not done with the whole list of products. We can get the five following products giving the endCursor value to an after variable from the same query as before:

query listProducts {
products(first: 5, after: "NQ") {
nodes {
name
slug
masterVariant {
defaultPrice {
amount
currency {
htmlEntity
}
}
images {
nodes {
smallUrl
}
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}

We could repeat the process until hasNextPage would be false.

We can also implement backward pagination if we switch first & after variables for last & before, and endCursor & hasNextPage fields for startCursor & hasPreviousPage. Relay's spec for cursor-based pagination is mainly intended to be used in infinite-scroll UX and, for now, it doesn't support bidirectional pagination (when paginating forward hasPreviousPage is meaningless and vice-versa).

Displaying a product

Once we have rendered the list of products, users would typically click on the one they're interested in. We can use its slug to display the complete information. For instance, the amazing Solidus T-Shirt is really appreciated by customers:

query getProduct {
productBySlug(slug: "solidus-t-shirt") {
name
description
variants {
nodes {
id
sku
position
prices {
nodes {
amount
currency {
htmlEntity
}
}
}
optionValues {
nodes {
name
presentation
optionType {
name
presentation
position
}
}
}
images {
nodes {
largeUrl
}
}
}
}
}
}

Notice that we're assuming there's not a crazy amount of product variants, nor its nested associations like prices and images. Otherwise, we could paginate those resources as we did before with products.

Adding a product to the cart

Say a visitor wants to buy two Solidus T-Shirt. Before adding the items to the cart, we need to create an order to attach the former to it. We need it so that we can group future additions and keep the state between requests.

Here comes our first GraphQL mutation. Mutations are similar to queries. By convention, they change the server's state. It's the equivalent to POST, PUT, PATCH and DELETEHTTP methods on traditional RESTful APIs.

We're going to use the createOrder mutation, and we need to make sure it returns the order's guestToken so that we can reference it back later. It's a good practice to always ask for errors in the mutation to detect any problem.

mutation createOrder{
createOrder(input: {}) {
order {
guestToken
number
state
}
errors {
path
message
}
}
}

Once we have the token, we need to set it as the value of a X-Spree-Order-Tokenrequest header. It's going to authenticate the request as coming from the same guest user.

We also need the id of the variant we want to add to the cart. If you look at the response for the product query, you'll see that if we chose the blue/small variant, its identifier corresponds to U3ByZWU6OlZhcmlhbnQtMTM=.

mutation addToCart {
addToCart(input: { variantId: "U3ByZWU6OlZhcmlhbnQtMTM=", quantity: 2}) {
order {
number
state
itemTotal
total
lineItems {
nodes {
amount
price
currency
quantity
variant {
id
}
hasSufficientStock
}
}
}
errors {
path
message
}
}
}

We could repeat the process for other items if needed.

Checkout 1 - Billing & shipping addresses

Once we're done adding items to the cart, we need to transition the checkout process to the next state:

mutation checkoutNextFromCart {
nextCheckoutState(input: {}) {
order {
number
state
}
errors {
path
message
}
}
}

Notice that the state changed from cart to address. That means we need to do a couple of things: associate an email to the order and set the user's shipping & billing addresses.

Let's go first with the email address:

mutation checkoutEmail {
setOrderEmail(input: { email: "[email protected]"} ) {
order {
number
state
email
}
}
}

All good. Before going with the addresses, we need to fetch country and state ids from our system. Here we have the list of countries:

query countries {
countries {
nodes {
id
name
}
}
}

Once users select a country, we want them to select a state from that country. We can leverage the generic node query, which accepts as an argument the id of any resource. node returns a Node interface, i.e., a polymorphic type. To access the concrete type, we need to pattern match with ... onGraphQL syntax:

query states {
node(id: "U3ByZWU6OkNvdW50cnktMjMz") {
... on Country {
states {
nodes {
id
name
}
}
}
}
}

We now have everything we need to create the addresses. We'll use the same for both shipping and billing:

mutation checkoutAddress {
addAddressesToCheckout(
input: {
billingAddress: {
name: "Alice"
address1: "1 Solidus Road"
city: "LA"
countryId: "U3ByZWU6OkNvdW50cnktMjMz"
zipcode: "65555"
phone: "111111"
stateId: "U3ByZWU6OlN0YXRlLTM0Mzk="
}
shipToBillingAddress: true
}
) {
order {
number
state
email
itemTotal
adjustmentTotal
total
billingAddress {
name
}
shippingAddress {
name
}
adjustments {
nodes {
label
amount
eligible
}
}
}
}
}

Pay attention to how $2.5 of adjustments were added to the total. That's because of a 5% tax applied due to the checkout address. It's something configurable from Settings -> Taxesin the admin section.

Checkout 2 - Shipment method

We can proceed now to the next checkout step: introducing the delivery data. Let's use the query to retrieve the available shipping rates for the generated shipment (be sure to have shipment methods associated with the shipping address. They're configurable from Settings -> Shipping in the admin section):

mutation checkoutNextFromAddress {
nextCheckoutState(input: {}) {
order {
number
state
itemTotal
adjustmentTotal
shipmentTotal
total
shipments {
nodes {
number
shippingRates {
nodes {
id
cost
currency
selected
shippingMethod {
name
carrier
}
}
}
}
}
adjustments {
nodes {
amount
label
eligible
}
}
}
errors {
path
message
}
}
}

We can see that the state changed to deliveryand that the cheapest shipping rate has been selected by default. Notice it carries with it the same 5% tax adjustment applied to its cost.

We're in a hurry as we want to wear those t-shirts as soon as possible! Let's choose the "One Day" method:

mutation checkoutShipment {
selectShippingRate(input: { shippingRateId: "U3ByZWU6OlNoaXBwaW5nUmF0ZS00OA==" }) {
order {
number
state
itemTotal
adjustmentTotal
shipmentTotal
total
shipments {
nodes {
number
shippingRates {
nodes {
id
cost
currency
selected
}
}
}
}
adjustments {
nodes {
amount
label
eligible
}
}
}
}
}

We can check that totals and taxes have been updated accordingly.

Checkout 3 - Payment method

We're one step closer to complete the checkout, but we still need to drop some money! First, we need to advance to the next step. But we'll take the occasion to retrieve the available payment methods:

mutation checkoutNextFromDelivery {
nextCheckoutState(input: {}) {
order {
number
state
total
availablePaymentMethods {
id
name
description
position
}
}
errors {
path
message
}
}
}

Let' use the bogus credit card option to recreate an actual credit card payment but with no real money involved:

mutation checkoutPayment {
addPaymentToCheckout(
input: {
paymentMethodId: "U3ByZWU6OlBheW1lbnRNZXRob2Q6OkJvZ3VzQ3JlZGl0Q2FyZC0y",
source: {
number: "4111111111111111",
name: "Alice",
expiry: "12/29",
verification_value: "123"
}
}
) {
order {
number
state
payments {
amount
state
paymentSource {
paymentMethod {
id
description
}
}
}
}
errors {
path
message
}
}
}

We can see how the payment information has been associated to the returned order.

danger

In most cases, it's better not to deal with such sensitive information as credit card numbers directly in your store. There're extensions to integrate third-party payment systems, like solidus_stripe or solidus_square. If you do need to create your custom payment system, please check out our payments section, and don't forget to ensure PCI compliance.

Checkout 4 - Confirm order

We still need to confirm our order. As in the previous states, we need to tell the system that we're ready for the next step:

mutation checkoutNextFromPayment {
nextCheckoutState(input: {}) {
order {
number
state
}
errors {
path
message
}
}
}

The state has changed to confirm, so we can finally complete the checkout:

mutation checkoutConfirm {
completeCheckout(input: {}) {
order {
number
state
payments {
state
}
}
errors {
path
message
}
}
}

Take a look at how the state automatically changed to complete. The state for the payment also changed from checkout to pending, and it'll move again to completed automatically or manually by an admin when the money is received. We have nothing else to do and the best part of it is that the Solidus t-shirts are on their way to us!

Authenticated users

Up to this point, we have gone through the checkout process with a guest user: a visitor whose data (besides the associated to the order itself) is not persisted in our system. However, the user model has a spree_api_keyfield, which can be used to identify users between requests. For signing in/up/out, you can rely on external extensions, like solidus_jwt or solidus_auth_devise.

But let's go back to what interests us now, the GraphQL extension, as quickly as possible. Open a Rails console:

bin/rails console

Then, create a new user, generate its spree_api_keyand write it down:

user = Spree::User.create(email: "[email protected]", password: "password")
user.generate_spree_api_key
# Copy the output from last line
user.save

Going back to your GraphQL client, remove the X-Spree-Order-Tokenheader and add instead a new Authorizationone. Its value must be Bearer {token}, where you need to substitute {token} by the spree_api_keyvalue you generated above.

At some point, you'd ask your user for a default ship address:

mutation addAddress {
saveInAddressBook(input: {
address: {
name: "Joe Doe"
address1: "1 E-commerce Rd."
city: "LA"
countryId: "U3ByZWU6OkNvdW50cnktMjMz"
zipcode: "36666"
phone: "222222"
stateId: "U3ByZWU6OlN0YXRlLTM0Mzk="
}
}) {
errors {
path
message
}
user {
shipAddress {
name
}
}
}
}

With an extra addressType argument, you could also create a billing address (the argument is an enum, and can take shipping or billing values —notice the absence of double quotes "):

mutation addAddress {
saveInAddressBook(input: {
address: {
name: "Joe Doe"
address1: "25 Ruby Av."
city: "LA"
countryId: "U3ByZWU6OkNvdW50cnktMjMz"
zipcode: "36666"
phone: "222222"
stateId: "U3ByZWU6OlN0YXRlLTM0Mzk="
},
addressType: billing
}) {
errors {
path
message
}
user {
billAddress {
name
}
}
}
}

The checkout process for a registered user is very similar to what we did before. We'd start once more with a createOrder mutation, but this time we don't need the guestToken field. Also, we'd add something to the cart through addToCart.

This time, there's no need to ask users for an email associated with the order nor their address as we already have them. We can also assume that, in the beginning, users could accept the default shipping method. That allows us to fast-forward the process and leave the order ready to be paid:

mutation checkoutAdvanceFromCart {
advanceCheckout(input: {}) {
order {
email
state
itemTotal
shipmentTotal
adjustmentTotal
shippingAddress {
name
}
shipments {
nodes {
number
shippingRates {
nodes {
id
cost
currency
selected
}
}
}
}
availablePaymentMethods {
id
name
description
position
}
adjustments {
nodes {
label
amount
eligible
}
}
}
}
}

Nothing forbids us to change the address with addAddressesToCheckoutmutation, or change the shipping rate with selectShippingRate. But, if the user is ok with what it's proposed, we would jump straight into addPaymentToCheckout and, again, complete the process with nextCheckoutState and completeCheckout.