Docs
Launch GraphOS Studio

Contracts usage patterns

contractsschema-design

💡 TIP

If you're an enterprise customer looking for more material on this topic, try the Enterprise best practices: Contracts course on Odyssey.

Not an enterprise customer? Learn about GraphOS for Enterprise.

Contracts enable teams to contribute to a single unified graph while also delivering use-case-specific s to different types of consumers. Graph administrators define filter rules to generate contract schemas that are a strict subset of the source .

There are three different filtering strategies you can use for a contract:

  • Selective exclusion (pruning)
  • Selective inclusion (deny by default)
  • Combined exclusion and inclusion

The sections below cover when to use each strategy and why.

Selective exclusion (pruning)

Defining an "excludes" filter for a contract requires the least work, however it also carries the highest risk of exposing new s if you forget to tag them appropriately. We recommend this strategy only in cases where accidental exposure of sensitive fields is acceptable (such as in a beta API), or if your organization has strict governance and review policies in place.

As an example, let's look at a single using a tag internal to denote internal-only s.

type User {
id: ID!
email: String!
username: String!
description: String
supportNotes: [String!]! @tag(name: "internal")
supportLevel: Int!
}

It's likely that supportLevel has the same sensitivity as supportNotes, but the developer left off the tag, so it's now exposed in the contract.

As a result, Apollo finds this approach best when it's acceptable to have s exposed in the contract, such as the case with beta or experimental APIs where having the beta fields in the production wouldn't cause concern.

Selective inclusion (deny by default)

Although the "includes" filter can be more cumbersome to use than "excludes", we recommend using it to create contracts in most cases for the following reasons:

  • It's more intuitive to reason with when working with the . Having clear markings on which s are exposed, alongside clear naming conventions, provides an improved developer experience when working with contracts.
  1. It's much more secure when compared to the "exclude" strategy, given that new and potentially sensitive s must be explicitly added to a contract.

To demonstrate why this is the case, let's take a look at the same type as before, but using an "include" model. This time, the company opted to use an external tag to denote which s are exposed to external consumers:

type User {
id: ID! @tag(name: "external")
email: String! @tag(name: "external")
username: String! @tag(name: "external")
description: String @tag(name: "external")
supportNotes: [String!]!
supportLevel: Int!
}

Instead of tagging the two support s to exclude them, we now explicitly denote which fields are exposed externally. This also more clearly conveys the intent of the tag—a developer would know that the id is exposed to external parties.

If the team wants to expose a new and forgets to tag it, it is not included in the resulting contract .

As a result, we recommend this approach when building anything that requires that only specific s are exposed to another audience, such as external or partner APIs.

Combined exclusion and inclusion

The last of the three options has specific use cases, but these are much more dependent on the you're working with. This filter method allows you to set a filter of included tags, but then refine that result by removing others.

We recommend considering this if you need to broadly include types (such as the above User type) but need to refine further with removing other s. As a result, this typically works in the same situations as the exclude approach, but has some benefits by allowing you to explicitly defining types/fields that should be included explicitly alongside it avoiding some of the pitfalls of the exclude-only approach.

As such, make sure to treat this as a combination of both of the previous approaches:

  • Because you still rely on an exclude filter to refine the results, treat this approach much like the exclude approach discussed above. It's possible to accidentally include s as a result of still further refining using an exclude filter.
  • Use the include filter to ensure only the types/s are being exposed that you want to expose.

To wrap up the examples, let's look at the previous example where we've refactored to use this filtering approach:

type User @tag(name:"external") {
id: ID!
email: String!
username: String!
description: String
support: SupportInformation! @tag(name: "internal")
}
type SupportInformation: @tag(name:"internal") {
level: Int!
supportNotes: [String!]!
}

Now that we've created a new type to house the support information, we tag that type with the internal tag. We'd use this tag to exclude the type from the resulting external API, but the general User type would be included without the support . If the support is not also tagged as internal, it results in a composition error, providing some safety.

By allowing types to be tagged, it's possible to create "guardrails" against missing tags. As noted above, if a type is tagged but a emitting that type is not, it fails that contract's composition. If the owning service can ensure that all types are tagged appropriately with the correct tag to filter, it can be a good way to still accomplish the exclude approach while getting some of the benefit of the include approach.

On the other hand, this does mean that a strong governance program must be in place. Otherwise, you can encounter the same issues as discussed in the exclude approach.

You can also use this strategy for documentation purposes, where including and omitting is much less of a concern compared to a realized API. Because a contract is just a graph , it's possible to link users to a contract's documentation, making it useful in this context as well.

Cross-subgraph tagging

When considering the strategies described above, there's an additional composition behavior to be aware of: it's possible for a tag applied in one to affect s that are defined in another .

As an example, consider these two s:

# Subgraph A
type Document @key(fields: "id") {
id: ID!
name: String
content: String
}
# Subgraph B
type Document @key(fields: "id") @tag(name: "internal") {
id: ID!
reviewedBy: AdminUser
approvedBy: AdminUser
contentScore: Float
}

As shown above, the Document type is tagged as internal by B, but not by A. When the contract is generated for this , the Document type definition looks like this:

type Document @key(fields: "id") @tag(name: "internal") {
id: ID!
name: String
content: String
reviewedBy: AdminUser
approvedBy: AdminUser
contentScore: Float
}

This is due to the fact that tag inheritance is applied after composition. Any tags added at the type level are inherited after cross- entity definitions are merged. This means that what might be considered the "correct" application of a tag within one subgraph can adversely affect other s and the resulting .

Next
Home
Edit on GitHubEditForumsDiscord