DEV Community

Valerio Francescangeli
Valerio Francescangeli

Posted on • Originally published at v19i.com

GraphQL: Publish the Schema of Your Company, Not Your Api

GraphQL is an interesting technology that promises better developer and user experience, but its usefulness can be misinterpreted as an overall better technology than what came before it.

To best utilise it, a specific organisation approach must be used. GraphQL works best when the whole organisation shares the same domain conventions. Domain Driven Design shines in a GraphQL environment, but it requires a lot of effort to change a company’s mentality and embrace it.

In this article, I’ll describe how GraphQL differs from a more common REST API approach and how to ensure a successful coexistence or migration.

HTTP REST API

The default way to build a service is over the internet. All languages and libraries understand it well, and its division into different verbs provides solutions for safe, idempotent and cacheable requests.

Let’s take a library API as an example. To add a new book or to publish a new review of a book, a REST API would look like this:

Method Endpoint Description
POST “/books” Create a new book
POST “/books/:id/reviews” Create a new review

This is fine in a typical CRUD style API, but a GraphQL one can instead leverage the business’ domain naming:

type Mutation {
  publishBook({book info})
  publishReview({review info})
}
Enter fullscreen mode Exit fullscreen mode

Differences between REST and GraphQL

Your schema is your API

This is a significant shift from the way your endpoint will expose data. To ensure a well-structured schema, start from the business problems and produce a cohesive shared understanding of the business. Domain Driven Design is what allows an organisation to create one and use it to define what your schema should look like.

Start by defining your domain events. What events happen in your business that are definable as a step forward? Create as many as you need. Once done, organise them in a timeline and group them by context. Each context will enable consistent use of the same words across all events, even when the same word means something different in a different context.

Some events will require business rules, and those rules will require data from internal or external sources. Define those rules and where the data is coming from. This will result in an action that will generate the new domain event.

This system will help define what structure your schema will have:

  • Internal resources are GraphQL Queries
  • External resources live outside of the schema and must be merged, together with the business rules, in the business layer logic
  • Actions are GraphQL Mutations, ready to be exposed on the schema

Remember that your schema is an ever-evolving system. Embrace it, and don’t be afraid to change it when they improve your customers’ experience.

Common mistake when migrating from REST

Don’t simply move your CRUD verbs. GraphQL’s queries and mutations can use names that make sense to the business, not an API convention.

Consider a typical REST API:

Method Endpoint Description
GET “books/” Get all books
GET “books/:id” Get a book
POST “books/“ Create a new book
POST “books/:id/review” Create a new review
PATCH “books/:id/review” Update a review

A common first step is to use the same style that REST adopts:

type Query {
  getAllBooks()
  getBook({id})
}

type Mutation {
  createBook({book data})
  createReview({bookID}, {review data})
  updateReview({bookID}, {review data})
}
Enter fullscreen mode Exit fullscreen mode
  1. There is no need to stick to REST verbs if it doesn’t make sense from a business perspective. It is better to use more straightforward and more effective names.
  2. Updates are handled differently in GraphQL. Use one big input object instead of dividing it in id and body.
type Query {
  books()
  book({id})
}

type Mutation {
  // include everything in one object, {bookID} should be included inside
  createReview({review data})
  updateReview({review data})
}
Enter fullscreen mode Exit fullscreen mode

Versioning

In REST, new versions are required to clarify in what shape your data will be returned, but it is an anti-pattern in GraphQL. Multiple versions mean multiple schemas, which will get out of hand quickly.
GraphQL’s way of dealing with changes is to add new fields when necessary and tag the old ones as deprecated. There is no drawback in having a lot of fields, as every client will always require only what they need.

Authorisation

There are no particular differences here, but if the plan is to keep both REST and GraphQL endpoint, it is better to move the authorisation logic down into the business layer to consolidate the checks in a single place and allow each endpoint to only care about passing down the correct data.

Query Complexity

The freedom allowed by GraphQL is a double-edged sword. Clients can optimise their queries for their use case, but what they need and ask at once might be onerous on your infrastructure. Load-testing your client’s use cases to verify how many records and how much nesting to allow must be considered when exposing your GraphQL endpoint to the public.

Rate limiting

Rate limiting an API can also be challenging. Consider how each part of your schema respond to an increase of queries and implement a business logic to prevent abuse.

Caching

Contrary to REST, caching in GraphQL doesn’t have a well-defined set of rules, with each client managing its cache. If one or more services rely heavily on caching to be functional, think twice before moving them to GraphQL, as it is a delicate matter even when using best practices.

Client Side Caching

Client side cache, with clients like Apollo, relies on cache deduplication and normalisation to optimise the fetching and re-fetching of data. This system works similarly to an SQL database, leveraging the knowledge of the graph architecture and the information about the query to know what to load and what to skip.

Let’s assume this query:

query GetAllBooks {
  books {
    id
    title
  }
}
Enter fullscreen mode Exit fullscreen mode

That returns this data:

{
  books: [
    {<book1>},
    {<book2>},
    {<book3>},
    {<...>},
  ]
}
Enter fullscreen mode Exit fullscreen mode

By default, the normalisation process will split the data to allow it to be individually cached:

{
  id: 1,
  __typename: "Book",
  title: "Book1",
}
Enter fullscreen mode Exit fullscreen mode

Apollo Client will use the __typename and id to create a unique pair that will use to cache it on the client, in a quick-access data structure, like this:

{
  "Book:1": { id: 1, title: "Book1" },
  "Book:<n>": { id: <n>, title: <title> },
}
Enter fullscreen mode Exit fullscreen mode
Notes on client side caching
  • It’s important to have a unique identifier; without it, Apollo Client won’t be able to keep track of changes to the object
  • The library creates an array that references the normalised data to keep the correct ordering in place.

Server Side Caching

Apollo Server supports server side caching. It can be applied to a type or a specific field by proving a maxAge value, which describes the number of seconds the cache will be considered valid.

These are the allowed properties to control the cache:

Name Description
maxAge Number of seconds the resource will be cached for. Default to 0.
scope Can be PUBLIC or PRIVATE. Use PRIVATE to make a response as specific to a single user Default is PUBLIC.
inheritMaxAge If set to true, it will inherit the maxAge value of its parent. Make sure not to provide maxAge if you plan to use inheritMaxAge.

It is used by using a directive called @cacheControl. The previous query for books could be cached for one day by having a type Book with this value:

type Book @cacheControl(maxAge 86400) {
  id: String!
  title: String!
}
Enter fullscreen mode Exit fullscreen mode

The caveat is that the query as a whole will be valid based on the field with the lowest maxAge value.

type Review {
  id: String!
  book: Book!
}
Enter fullscreen mode Exit fullscreen mode
A few caching rules
  • Having maxAge set to 0 by default means nothing is cached out of the box. It is easier to start without cache and add it where needed, avoiding unexpected behaviours by caching too early.
  • If any field inside a query has a smaller maxAge value, the whole result will have that cache value associated with it.
  • If any field inside a query is set to PRIVATE, the whole response will be marked as PRIVATE.

Here, I’ve discussed statically set cache. Note that there is a way to set the cache dynamically. This is only a small introduction to a deep topic.

Conclusion

This article barely scratched what GraphQL can do for an organisation. The simplicity given by being able to organise and find data in the same way it is talked about from the business is a benefit that enables a more cohesive discussion between engineering and product, and it should not be considered an extra but instead seen as the core of it.

Top comments (0)