-
Notifications
You must be signed in to change notification settings - Fork 120
docs(blog): public graphql schema federation contracts #6952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
228 changes: 228 additions & 0 deletions
228
...b/docs/src/app/blog/(posts)/public-graphql-schema-federation-contracts/page.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
--- | ||
title: Incrementally Exposing a Public GraphQL API with Federation Contracts | ||
authors: laurin | ||
tags: [federation, graphql, hive] | ||
date: 2025-09-22 | ||
description: | ||
'Learn how to safely expose a public GraphQL API from a monolith using federation contracts and | ||
tags, enabling incremental and controlled schema evolution.' | ||
--- | ||
|
||
Many teams start with a **single GraphQL monolith** powering their applications. Over time, the need | ||
arises to expose parts of that schema publicly - whether for partners, customers, or other external | ||
integrations. | ||
|
||
But here’s the problem: | ||
|
||
Your internal schema wasn’t designed for public consumption. It likely contains inconsistent naming, | ||
experimental fields, and sensitive operations you don’t want outsiders touching. | ||
|
||
At the same time, you don’t want to maintain multiple APIs - one for internal use and another for | ||
public users. That leads to duplication of business logic, increased maintenance burden, and the | ||
constant risk of the two drifting out of sync. Having **a single source of truth in one API** | ||
ensures consistency, reduces overhead, and allows you to evolve your system with confidence. | ||
|
||
So how do you **evolve a monolithic GraphQL schema into a safe, public API** while keeping | ||
everything unified? | ||
|
||
The answer: **GraphQL Federation** and **Schema Contracts**. | ||
|
||
## Step 1: Treat the Monolith as a Subgraph | ||
|
||
Before exposing your schema, the first step is to make your monolith federation-compatible. | ||
|
||
Federation is often associated with microservices, but you don’t need dozens of subgraphs to benefit | ||
from it. A monolithic schema can also be treated as a subgraph. All it takes is a few federation | ||
directives: | ||
|
||
```graphql | ||
extend schema | ||
@link(url: "https://specs.apollo.dev/link/v1.0") | ||
@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@tag"]) | ||
``` | ||
|
||
Now your monolith can participate in the same contract-based filtering that federated graphs use. | ||
|
||
## Step 2: Use Tags to Mark What’s Public | ||
|
||
Next, we need a way to label which parts of the schema are safe to expose. The `@tag` directive is a | ||
simple but powerful tool for this: | ||
|
||
```graphql | ||
type Query { | ||
publicInfo: String @tag(name: "public") | ||
privateInfo: String | ||
} | ||
``` | ||
|
||
By tagging fields, you can later generate a **contract schema** that only includes the safe, | ||
public-facing parts of your internal API. | ||
|
||
--- | ||
|
||
## Step 3: Define a Public Contract | ||
|
||
Once tagging is in place, you can generate a **contract schema**: | ||
|
||
```graphql | ||
type Query { | ||
publicInfo: String | ||
} | ||
``` | ||
|
||
This filtered contract becomes your **public API schema**, while your full internal schema continues | ||
to serve your own applications. | ||
|
||
There are a few ways to create a contract schema: | ||
|
||
**1. Using Hive Console or other schema registry** | ||
|
||
If you’re working with a hosted schema registry, like | ||
[**Hive Console**](https://the-guild.dev/graphql/hive), you can: | ||
|
||
- Define a new contract and select which tags (e.g., `public`) to include. | ||
- Automatically generate and validate the filtered schema whenever a new subgraph is published. | ||
- Take advantage of features like usage analytics and breaking change detection to **collaborate | ||
safely** and ensure consistency across contributors. | ||
|
||
[Learn more in the Hive Console documentation](https://the-guild.dev/graphql/hive/docs/schema-registry/contracts) | ||
|
||
**2. Using a CLI or Library** | ||
|
||
If you not yet have adopted a schema registry, you can also use our | ||
[MIT licensed JavaScript library for Federation Composition](https://github.com/graphql-hive/federation-composition) | ||
to generate the contract programmatically from your monolith. | ||
|
||
```ts | ||
import { parse } from 'graphql' | ||
import { composeSchemaContract } from '@theguild/federation-composition' | ||
|
||
const result = composeSchemaContract( | ||
[ | ||
{ | ||
name: 'monolith', | ||
typeDefs: parse(/* GraphQL */ ` | ||
type Query { | ||
publicInfo: String @tag(name: "public") | ||
privateInfo: String | ||
} | ||
`) | ||
} | ||
], | ||
/** Tags to include and exclude */ | ||
{ | ||
include: new Set(['public']), | ||
exclude: new Set() | ||
}, | ||
/** Exclude unreachable types */ | ||
true | ||
) | ||
|
||
// This is the filtered schema! | ||
console.log(result.publicSdl) | ||
``` | ||
|
||
Then you can simply create a private schema, similar to the following | ||
|
||
```ts | ||
import { createSchema } from 'graphql-yoga' | ||
import { composeSchemaContract } from '@theguild/federation-composition' | ||
import { resolvers } from './resolvers' | ||
|
||
// ... | ||
|
||
const publicSchema = createSchema({ | ||
typeDefs: parse(result.publicSdl), | ||
resolvers, | ||
resolverValidationOptions: { | ||
// The resolvers still contain the ones of the public schema | ||
// Instead of filtering them out ignoring it is good enough. | ||
requireResolversToMatchSchema: 'ignore' | ||
} | ||
}) | ||
``` | ||
|
||
## Step 4: Serve the Public Schema Contract | ||
|
||
Creating a filtered schema is only useful if clients can actually query it. Once you have your | ||
contract, you need to **serve it as your public API**. | ||
|
||
The good news: **any federation-compatible router that supports supergraphs can serve a federation | ||
contract**. Popular choices include Apollo Gateway, | ||
[Hive Gateway](https://the-guild.dev/graphql/hive/gateway), or | ||
[Hive Router](https://github.com/graphql-hive/router). | ||
|
||
If using Hive Console as a schema registry, point your gateway to | ||
[the contract supergraph endpoint](https://the-guild.dev/graphql/hive/docs/schema-registry/contracts#access-contract-cdn-artifacts) | ||
to have it expose the public API. | ||
|
||
Additionally, you can then configure things like authentication, rate limiting, and access policies. | ||
|
||
Clients can now consume the public API fields by pointing to the gateway, while the internal schema | ||
remains private. | ||
|
||
As a additional security measure you should leverage | ||
[persisted documents to avoid execution of arbitary GraphQL operations](https://the-guild.dev/graphql/hive/docs/gateway/persisted-documents) | ||
against the private schema. | ||
|
||
For more guidance on choosing a gateway for your project, refer to the | ||
[Federation Gateway Audit](https://the-guild.dev/graphql/hive/federation-gateway-audit) for feature | ||
compatibility and the | ||
[Federation Gateway Performance Benchmark](https://the-guild.dev/graphql/hive/federation-gateway-performance) | ||
for performance considerations. | ||
|
||
As mentioned before, if you are not relying on a schema registry you can simply use and GraphQL | ||
server for serving the public schema. | ||
|
||
```ts filename="Example GraphQL Yoga" | ||
import { createServer } from 'node:http' | ||
import { createSchema, createYoga } from 'graphql-yoga' | ||
import { publicSchema } from './public-schema' | ||
|
||
const server = createServer( | ||
createYoga({ | ||
schema: publicSchema | ||
}) | ||
) | ||
|
||
server.listen(8080) | ||
``` | ||
|
||
## Step 5: Evolve the Public Schema Incrementally | ||
|
||
Federation contracts let you add fields to the public schema **at your own pace**. | ||
|
||
For example, when you decide to open up a mutation: | ||
|
||
```graphql | ||
input PublishInput @tag(name: "public") { | ||
data: String! | ||
} | ||
|
||
type Mutation { | ||
publishData(input: PublishInput!): PublishResult! @tag(name: "public") | ||
} | ||
``` | ||
|
||
Tag it, release the new version of your GraphQL schema, regenerate the contract, and the public | ||
schema expands automatically. | ||
|
||
Iterate and refactor your schema internally, then make it public when you are ready. | ||
|
||
No risky schema forks, no duplication, just maintain a **single, unified GraphQL API** while safely | ||
evolving your public interface. | ||
|
||
## Conclusion | ||
|
||
GraphQL Federation isn’t just for distributed architectures. It’s also a powerful tool for | ||
**partitioning access within a monolith**. | ||
|
||
By combining federation contracts with tagging, you can safely evolve a private schema into a public | ||
one, while only exposing the parts you want today, and leaving the door open for more tomorrow. | ||
|
||
This approach provides a clean, incremental path to offering a public GraphQL API without | ||
compromising the flexibility of your internal schema. | ||
|
||
[Learn more on schema contracts with Hive | ||
Console]([Learn more in the Hive Console documentation](https://the-guild.dev/graphql/hive/docs/schema-registry/contracts). | ||
). |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.