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

You can also use our MIT licensed JavaScript library for Federation Composition to generate the contract programmatically from your monolith.

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.

When serving your contract you deploy the Gateway that consumes the schema contract from the schema registry to your infrastructure.

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 measure you can 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.

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:

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.

Last updated on