Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
).
Loading