This is documentation for v16, which is currently in preview.
See the latest stable version instead.

Adding a Subgraph

Adding a Subgraph

You have an existing Fusion project -- a gateway, one or more subgraphs, and a working composition pipeline -- and you need to add a new subgraph. Maybe your team owns a new domain (shipping, billing, inventory), or you are splitting an existing subgraph into smaller services. Either way, the process is the same: create a new HotChocolate project, define your types and any entity extensions, export the schema, compose, and verify.

This page walks through adding a Shipping subgraph to an existing project that already has Products and Reviews subgraphs. If you have not set up a Fusion project yet, start with the Getting Started tutorial first.

Prerequisites

Before you begin, you need:

  • An existing Fusion project with at least one subgraph and a gateway
  • The Nitro CLI installed (dotnet tool install -g ChilliCream.Nitro.CLI)
  • The .NET 10 SDK or later

You should be able to compose and run your existing project successfully. If composition is currently broken, fix that first.

Subgraph Project Structure

Every Fusion subgraph follows the same structure. Here is the canonical layout based on the fusion-demo patterns:

src/
Shipping/
Types/
Product.cs # Entity stubs and type extensions
Query.cs # Query resolvers (lookups)
Program.cs # Server configuration
Shipping.csproj # Project file
appsettings.json # App settings
schema-settings.json # Fusion subgraph settings (created on first schema export)
schema.graphqls # Exported schema (generated, do not edit)

The Types/ directory holds your GraphQL type definitions. The schema.graphqls and schema-settings.json files are generated by schema export -- you edit schema-settings.json but never edit schema.graphqls directly.

For projects with data access, you would also have a Data/ directory for your entity classes, DbContext, and DataLoaders:

src/
Shipping/
Data/
ShipmentContext.cs # EF Core DbContext
Shipment.cs # Entity class
ShipmentDataLoader.cs # DataLoader for batching
Types/
...

Create the Subgraph Project

Create a new ASP.NET Core web project and add the required HotChocolate packages:

Bash
dotnet new web -n Shipping
cd Shipping
dotnet add package HotChocolate.AspNetCore --version "16.0.0-p.11.2"
dotnet add package HotChocolate.AspNetCore.CommandLine --version "16.0.0-p.11.2"
dotnet add package HotChocolate.Types.Analyzers --version "16.0.0-p.11.2"
cd ..

The three packages serve different purposes:

  • HotChocolate.AspNetCore -- The GraphQL server
  • HotChocolate.AspNetCore.CommandLine -- Enables dotnet run -- schema export for schema generation
  • HotChocolate.Types.Analyzers -- Source generator that auto-registers your types

Your Shipping.csproj should look like this:

XML
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HotChocolate.AspNetCore" Version="16.0.0-p.11.2" />
<PackageReference Include="HotChocolate.AspNetCore.CommandLine" Version="16.0.0-p.11.2" />
<PackageReference Include="HotChocolate.Types.Analyzers" Version="16.0.0-p.11.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

If your project uses shared defaults (like the fusion-demo's SourceSchemaDefaults project for shared GraphQL configuration), add a project reference to it as well. Shared defaults keep configuration like AddGlobalObjectIdentification(), AddMutationConventions(), and AddInstrumentation() consistent across all subgraphs.

Configure the Server

Replace the contents of Program.cs:

C#
// Shipping/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddGraphQLServer()
.AddTypes();
var app = builder.Build();
app.MapGraphQL();
app.RunWithGraphQLCommands(args);

This is the same minimal setup as any HotChocolate subgraph. AddTypes() is source-generated and registers all types discovered by the analyzer. RunWithGraphQLCommands(args) enables CLI commands like schema export.

If your project has shared defaults, replace .AddTypes() with your shared configuration:

C#
builder
.AddGraphQL("shipping-api")
.AddDefaultSettings()
.AddShippingTypes();

The string "shipping-api" is the schema name used for schema export naming.

Define Your Types

The Shipping subgraph adds a deliveryEstimate field to the existing Product type. It does not own the Product -- the Products subgraph does. The Shipping subgraph extends it.

Create the Entity Stub

An entity stub is a lightweight declaration that says "I know this type exists in the graph and I want to add fields to it." Create Types/Product.cs:

C#
// Shipping/Types/Product.cs
namespace Shipping.Types;
[EntityKey("id")]
public sealed record Product([property: ID<Product>] int Id)
{
public int GetDeliveryEstimate(
string zip,
[Require(
"""
{
weight,
length: dimension.length
width: dimension.width
height: dimension.height
}
""")]
ProductDimensionInput dimension)
{
const int baseDays = 2;
var volumeCm3 = dimension.Length * dimension.Width * dimension.Height;
var sizeDays = volumeCm3 > 50000 ? 2 : 0;
var weightDays = dimension.Weight > 500 ? 1 : 0;
return Math.Max(1, baseDays + sizeDays + weightDays);
}
}

Several things to notice:

  • [EntityKey("id")] explicitly declares that Product is an entity identified by the id field. This is needed here because the Shipping subgraph does not have its own lookup that the composition engine can use to infer the key automatically. When a subgraph has a [Lookup] resolver (like GetProductById(int id)), the composition engine infers the key from the lookup's arguments. When there is no lookup, use [EntityKey] to declare the key explicitly.
  • record Product(int Id) is the entity stub. It only contains the Id field -- it does not duplicate name, price, or any other field from the Products subgraph.
  • [Require(...)] declares that the deliveryEstimate resolver needs data from other subgraphs. The weight field and dimension.length, dimension.width, dimension.height fields all live in the Products subgraph. The gateway fetches these values automatically and passes them as the dimension argument. Clients never see this argument -- it is removed from the composite schema.

Create the input type for the required dimension data:

C#
// Shipping/Types/ProductDimensionInput.cs
namespace Shipping.Types;
public sealed class ProductDimensionInput
{
public int Weight { get; init; }
public double Length { get; init; }
public double Width { get; init; }
public double Height { get; init; }
}

Add the Lookup

The gateway needs a way to enter the Shipping subgraph's Product type. Create Types/Query.cs:

C#
// Shipping/Types/Query.cs
namespace Shipping.Types;
[QueryType]
public static partial class Query
{
[Lookup, Internal]
public static Product GetProductById([ID<Product>] int id)
=> new(id);
}
  • [Lookup] marks this as an entity resolution entry point for the gateway.
  • [Internal] hides this lookup from the composite schema. Clients cannot call productById on the Shipping subgraph directly -- it exists only for the gateway's internal use during entity resolution.

The internal lookup constructs a Product stub from the ID without checking whether the product exists. This is safe because the gateway only calls internal lookups during entity resolution, after another subgraph has already confirmed the entity exists.

When to Use [BindMember]

If your subgraph stores a foreign key reference to an entity from another subgraph, use [BindMember] to replace the raw ID field with a resolved entity reference. This is common when your subgraph has its own data that references entities from other subgraphs.

For example, if the Shipping subgraph had a Shipment type with a productId field:

C#
// Types/ShipmentNode.cs
[ObjectType<Shipment>]
internal static partial class ShipmentNode
{
[BindMember(nameof(Shipment.ProductId))]
public static Product GetProduct(
[Parent(requires: nameof(Shipment.ProductId))] Shipment shipment)
=> new(shipment.ProductId);
}
  • [BindMember(nameof(Shipment.ProductId))] tells HotChocolate to replace the productId field on Shipment with the product field returned by this resolver.
  • [Parent(requires: nameof(Shipment.ProductId))] tells the gateway that it needs the ProductId from the parent Shipment object to resolve this field.

In the exported schema, clients see shipment.product (returning a full Product) instead of shipment.productId (a raw integer). The gateway resolves the full Product from whichever subgraph owns it.

The Shipping subgraph in this example does not have its own data, so it does not need [BindMember]. But if your new subgraph has entities that reference types from other subgraphs, this pattern is essential.

Configure schema-settings.json

When you export the schema for the first time, HotChocolate generates a schema-settings.json file alongside the schema.graphqls file. This file tells the composition engine about your subgraph. You need to edit it to set the correct values.

Here is the complete format:

JSON
{
"name": "shipping-api",
"transports": {
"http": {
"url": "{{API_URL}}",
"clientName": "fusion"
}
},
"extensions": {
"nitro": {
"apiId": "your-nitro-api-id"
}
},
"environments": {
"aspire": {
"API_URL": "http://localhost:5003/graphql"
},
"dev": {
"API_URL": "https://dev.example.com/graphql"
},
"prod": {
"API_URL": "https://api.example.com/graphql"
}
}
}

Field Reference

name (required) -- The unique identifier for this subgraph in the composed graph. This must be unique across all subgraphs. Convention: use lowercase with hyphens, matching the service name (e.g., shipping-api, products-api, accounts-api).

transports.http.url (required) -- The URL where the gateway can reach this subgraph at runtime. You can use a literal URL like http://localhost:5003/graphql or a template variable like {{API_URL}} that gets resolved from the environments section.

transports.http.clientName (optional) -- The named HTTP client the gateway uses to send requests to this subgraph. Defaults to "fusion". The gateway must register an HttpClient with this name (e.g., builder.Services.AddHttpClient("fusion")). All subgraphs in the fusion-demo use the same client name "fusion", which means they share a single HTTP client configuration on the gateway.

extensions.nitro.apiId (optional) -- The Nitro cloud API identifier. Only needed if you use Nitro for schema delivery and monitoring. You can omit the entire extensions section for local-only development.

environments (optional) -- Per-environment variable substitutions. Each key is an environment name, and the values are key-value pairs that replace {{VARIABLE}} placeholders in the url field. When you run nitro fusion compose --environment aspire, the CLI resolves {{API_URL}} to http://localhost:5003/graphql.

For a simple local setup without environment variables, the minimal schema-settings.json is:

JSON
{
"name": "shipping-api",
"transports": {
"http": {
"url": "http://localhost:5003/graphql"
}
}
}

Export the Schema and Compose

Export

From the project root, export the schema:

Bash
dotnet run ./Shipping -- schema export

This generates two files in the Shipping directory:

  • schema.graphqls -- The subgraph's GraphQL schema
  • schema-settings.json -- The subgraph settings (edit this as described above)

After export, open schema-settings.json and verify or update the name and url fields.

Compose

Run composition with all subgraph schemas, including your new one:

Bash
nitro fusion compose \
--source-schema-file Products/schema.graphqls \
--source-schema-file Reviews/schema.graphqls \
--source-schema-file Shipping/schema.graphqls \
--archive gateway.far

If composition succeeds, copy the updated gateway.far to your gateway project directory:

Bash
cp gateway.far Gateway/gateway.far

Troubleshooting Composition Errors

If composition fails after adding your new subgraph, the error messages point to specific issues. Common problems:

"Field X is defined in multiple subgraphs without [Shareable]" -- Your new subgraph defines a field that already exists in another subgraph. Key fields (like id) are automatically shareable, but all other duplicated fields need [Shareable] on every definition. See Entities and Lookups for details.

"No lookup found for entity X" -- Your subgraph references an entity type but no subgraph provides a lookup for it. Add a [Lookup] resolver (public or internal) for that entity.

"Incompatible field types" -- Two subgraphs define the same field with different types. The types must be compatible according to the composition merging rules.

Test Cross-Subgraph Queries

Start all services and the gateway. With the Shipping subgraph added, you can now query delivery estimates that cross subgraph boundaries:

GraphQL
query {
productById(id: 1) {
name
price
deliveryEstimate(zip: "10001")
}
}

This query touches three subgraphs:

  1. The gateway calls the Products subgraph to fetch name, price, weight, and dimension data
  2. The gateway passes the weight and dimensions to the Shipping subgraph as required arguments
  3. The Shipping subgraph calculates the delivery estimate and returns it
  4. The gateway merges everything into a single response

The client sees one unified response. The deliveryEstimate field's zip argument is visible to clients, but the dimension argument (marked with [Require]) is hidden -- the gateway fills it in automatically.

Next Steps

  • Need cross-subgraph field dependencies? If your subgraph's resolvers need data from other subgraphs (like the Shipping subgraph needing product weight), the [Require] attribute enables this. Cross-subgraph data dependencies will be covered in detail in future documentation.
  • Composition failed and you don't know why? See Composition for the full merging rules, common errors, and fixes.
  • Want to understand entities more deeply? See Entities and Lookups for the complete guide to entity stubs, public vs. internal lookups, field ownership, and [Shareable].
  • Ready to deploy? See Deployment and CI/CD for setting up independent subgraph deployments with the Nitro CLI.
Last updated on February 17, 2026 by Michael Staib