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:

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:

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:

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, 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

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 to generate the contract programmatically from your monolith.

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

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, or Hive Router.

If using Hive Console as a schema registry, point your gateway to the contract supergraph endpoint 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 against the private schema.

For more guidance on choosing a gateway for your project, refer to the Federation Gateway Audit for feature compatibility and the Federation Gateway Performance Benchmark 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.

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:

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. ).

Last updated on