|
| 1 | +--- |
| 2 | +title: Incrementally Exposing a Public GraphQL API with Federation Contracts |
| 3 | +authors: laurin |
| 4 | +tags: [federation, graphql, hive] |
| 5 | +date: 2025-09-22 |
| 6 | +description: |
| 7 | + 'Learn how to safely expose a public GraphQL API from a monolith using federation contracts and |
| 8 | + tags, enabling incremental and controlled schema evolution.' |
| 9 | +--- |
| 10 | + |
| 11 | +Many teams start with a **single GraphQL monolith** powering their applications. Over time, the need |
| 12 | +arises to expose parts of that schema publicly - whether for partners, customers, or other external |
| 13 | +integrations. |
| 14 | + |
| 15 | +But here’s the problem: |
| 16 | + |
| 17 | +Your internal schema wasn’t designed for public consumption. It likely contains inconsistent naming, |
| 18 | +experimental fields, and sensitive operations you don’t want outsiders touching. |
| 19 | + |
| 20 | +At the same time, you don’t want to maintain multiple APIs - one for internal use and another for |
| 21 | +public users. That leads to duplication of business logic, increased maintenance burden, and the |
| 22 | +constant risk of the two drifting out of sync. Having **a single source of truth in one API** |
| 23 | +ensures consistency, reduces overhead, and allows you to evolve your system with confidence. |
| 24 | + |
| 25 | +So how do you **evolve a monolithic GraphQL schema into a safe, public API** while keeping |
| 26 | +everything unified? |
| 27 | + |
| 28 | +The answer: **GraphQL Federation** and **Schema Contracts**. |
| 29 | + |
| 30 | +## Step 1: Treat the Monolith as a Subgraph |
| 31 | + |
| 32 | +Before exposing your schema, the first step is to make your monolith federation-compatible. |
| 33 | + |
| 34 | +Federation is often associated with microservices, but you don’t need dozens of subgraphs to benefit |
| 35 | +from it. A monolithic schema can also be treated as a subgraph. All it takes is a few federation |
| 36 | +directives: |
| 37 | + |
| 38 | +```graphql |
| 39 | +extend schema |
| 40 | + @link(url: "https://specs.apollo.dev/link/v1.0") |
| 41 | + @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@tag"]) |
| 42 | +``` |
| 43 | + |
| 44 | +Now your monolith can participate in the same contract-based filtering that federated graphs use. |
| 45 | + |
| 46 | +## Step 2: Use Tags to Mark What’s Public |
| 47 | + |
| 48 | +Next, we need a way to label which parts of the schema are safe to expose. The `@tag` directive is a |
| 49 | +simple but powerful tool for this: |
| 50 | + |
| 51 | +```graphql |
| 52 | +type Query { |
| 53 | + publicInfo: String @tag(name: "public") |
| 54 | + privateInfo: String |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +By tagging fields, you can later generate a **contract schema** that only includes the safe, |
| 59 | +public-facing parts of your internal API. |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +## Step 3: Define a Public Contract |
| 64 | + |
| 65 | +Once tagging is in place, you can generate a **contract schema**: |
| 66 | + |
| 67 | +```graphql |
| 68 | +type Query { |
| 69 | + publicInfo: String |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +This filtered contract becomes your **public API schema**, while your full internal schema continues |
| 74 | +to serve your own applications. |
| 75 | + |
| 76 | +There are a few ways to create a contract schema: |
| 77 | + |
| 78 | +**1. Using Hive Console or other schema registry** |
| 79 | + |
| 80 | +If you’re working with a hosted schema registry, like |
| 81 | +[**Hive Console**](https://the-guild.dev/graphql/hive), you can: |
| 82 | + |
| 83 | +- Define a new contract and select which tags (e.g., `public`) to include. |
| 84 | +- Automatically generate and validate the filtered schema whenever a new subgraph is published. |
| 85 | +- Take advantage of features like usage analytics and breaking change detection to **collaborate |
| 86 | + safely** and ensure consistency across contributors. |
| 87 | + |
| 88 | +[Learn more in the Hive Console documentation](https://the-guild.dev/graphql/hive/docs/schema-registry/contracts) |
| 89 | + |
| 90 | +**2. Using a CLI or Library** |
| 91 | + |
| 92 | +If you not yet have adopted a schema registry, you can also use our |
| 93 | +[MIT licensed JavaScript library for Federation Composition](https://github.com/graphql-hive/federation-composition) |
| 94 | +to generate the contract programmatically from your monolith. |
| 95 | + |
| 96 | +```ts |
| 97 | +import { parse } from 'graphql' |
| 98 | +import { composeSchemaContract } from '@theguild/federation-composition' |
| 99 | + |
| 100 | +const result = composeSchemaContract( |
| 101 | + [ |
| 102 | + { |
| 103 | + name: 'monolith', |
| 104 | + typeDefs: parse(/* GraphQL */ ` |
| 105 | + type Query { |
| 106 | + publicInfo: String @tag(name: "public") |
| 107 | + privateInfo: String |
| 108 | + } |
| 109 | + `) |
| 110 | + } |
| 111 | + ], |
| 112 | + /** Tags to include and exclude */ |
| 113 | + { |
| 114 | + include: new Set(['public']), |
| 115 | + exclude: new Set() |
| 116 | + }, |
| 117 | + /** Exclude unreachable types */ |
| 118 | + true |
| 119 | +) |
| 120 | + |
| 121 | +// This is the filtered schema! |
| 122 | +console.log(result.publicSdl) |
| 123 | +``` |
| 124 | + |
| 125 | +Then you can simply create a private schema, similar to the following |
| 126 | + |
| 127 | +```ts |
| 128 | +import { createSchema } from 'graphql-yoga' |
| 129 | +import { composeSchemaContract } from '@theguild/federation-composition' |
| 130 | +import { resolvers } from './resolvers' |
| 131 | + |
| 132 | +// ... |
| 133 | + |
| 134 | +const publicSchema = createSchema({ |
| 135 | + typeDefs: parse(result.publicSdl), |
| 136 | + resolvers, |
| 137 | + resolverValidationOptions: { |
| 138 | + // The resolvers still contain the ones of the public schema |
| 139 | + // Instead of filtering them out ignoring it is good enough. |
| 140 | + requireResolversToMatchSchema: 'ignore' |
| 141 | + } |
| 142 | +}) |
| 143 | +``` |
| 144 | + |
| 145 | +## Step 4: Serve the Public Schema Contract |
| 146 | + |
| 147 | +Creating a filtered schema is only useful if clients can actually query it. Once you have your |
| 148 | +contract, you need to **serve it as your public API**. |
| 149 | + |
| 150 | +The good news: **any federation-compatible router that supports supergraphs can serve a federation |
| 151 | +contract**. Popular choices include Apollo Gateway, |
| 152 | +[Hive Gateway](https://the-guild.dev/graphql/hive/gateway), or |
| 153 | +[Hive Router](https://github.com/graphql-hive/router). |
| 154 | + |
| 155 | +If using Hive Console as a schema registry, point your gateway to |
| 156 | +[the contract supergraph endpoint](https://the-guild.dev/graphql/hive/docs/schema-registry/contracts#access-contract-cdn-artifacts) |
| 157 | +to have it expose the public API. |
| 158 | + |
| 159 | +Additionally, you can then configure things like authentication, rate limiting, and access policies. |
| 160 | + |
| 161 | +Clients can now consume the public API fields by pointing to the gateway, while the internal schema |
| 162 | +remains private. |
| 163 | + |
| 164 | +As a additional security measure you should leverage |
| 165 | +[persisted documents to avoid execution of arbitary GraphQL operations](https://the-guild.dev/graphql/hive/docs/gateway/persisted-documents) |
| 166 | +against the private schema. |
| 167 | + |
| 168 | +For more guidance on choosing a gateway for your project, refer to the |
| 169 | +[Federation Gateway Audit](https://the-guild.dev/graphql/hive/federation-gateway-audit) for feature |
| 170 | +compatibility and the |
| 171 | +[Federation Gateway Performance Benchmark](https://the-guild.dev/graphql/hive/federation-gateway-performance) |
| 172 | +for performance considerations. |
| 173 | + |
| 174 | +As mentioned before, if you are not relying on a schema registry you can simply use and GraphQL |
| 175 | +server for serving the public schema. |
| 176 | + |
| 177 | +```ts filename="Example GraphQL Yoga" |
| 178 | +import { createServer } from 'node:http' |
| 179 | +import { createSchema, createYoga } from 'graphql-yoga' |
| 180 | +import { publicSchema } from './public-schema' |
| 181 | + |
| 182 | +const server = createServer( |
| 183 | + createYoga({ |
| 184 | + schema: publicSchema |
| 185 | + }) |
| 186 | +) |
| 187 | + |
| 188 | +server.listen(8080) |
| 189 | +``` |
| 190 | + |
| 191 | +## Step 5: Evolve the Public Schema Incrementally |
| 192 | + |
| 193 | +Federation contracts let you add fields to the public schema **at your own pace**. |
| 194 | + |
| 195 | +For example, when you decide to open up a mutation: |
| 196 | + |
| 197 | +```graphql |
| 198 | +input PublishInput @tag(name: "public") { |
| 199 | + data: String! |
| 200 | +} |
| 201 | + |
| 202 | +type Mutation { |
| 203 | + publishData(input: PublishInput!): PublishResult! @tag(name: "public") |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +Tag it, release the new version of your GraphQL schema, regenerate the contract, and the public |
| 208 | +schema expands automatically. |
| 209 | + |
| 210 | +Iterate and refactor your schema internally, then make it public when you are ready. |
| 211 | + |
| 212 | +No risky schema forks, no duplication, just maintain a **single, unified GraphQL API** while safely |
| 213 | +evolving your public interface. |
| 214 | + |
| 215 | +## Conclusion |
| 216 | + |
| 217 | +GraphQL Federation isn’t just for distributed architectures. It’s also a powerful tool for |
| 218 | +**partitioning access within a monolith**. |
| 219 | + |
| 220 | +By combining federation contracts with tagging, you can safely evolve a private schema into a public |
| 221 | +one, while only exposing the parts you want today, and leaving the door open for more tomorrow. |
| 222 | + |
| 223 | +This approach provides a clean, incremental path to offering a public GraphQL API without |
| 224 | +compromising the flexibility of your internal schema. |
| 225 | + |
| 226 | +[Learn more on schema contracts with Hive |
| 227 | +Console]([Learn more in the Hive Console documentation](https://the-guild.dev/graphql/hive/docs/schema-registry/contracts). |
| 228 | +). |
0 commit comments