website/src/docs/hotchocolate/v16/guides/openapi-adapter.md
The OpenAPI adapter exposes your Hot Chocolate GraphQL schema as REST endpoints with automatic OpenAPI documentation. You define GraphQL operations annotated with @http directives, and the adapter generates HTTP endpoints that accept REST-style requests, execute the underlying GraphQL operation, and return the result as JSON. The generated endpoints appear in your OpenAPI specification alongside any other ASP.NET Core endpoints.
This is useful when you have a GraphQL API and need to provide a REST interface for clients that do not support GraphQL, or when you want to offer both GraphQL and REST access to the same backend.
Install the HotChocolate.Adapters.OpenApi package:
dotnet add package HotChocolate.Adapters.OpenApi
Register the adapter on your GraphQL server and map the endpoints:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddRouting()
.AddOpenApi(options => options.AddGraphQLTransformer());
builder
.AddGraphQL()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddOpenApiDefinitionStorage(myStorage);
var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapOpenApi();
endpoints.MapOpenApiEndpoints();
endpoints.MapGraphQL();
});
app.Run();
AddOpenApiDefinitionStorage() registers the adapter services and provides the endpoint definitions. AddGraphQLTransformer() adds a document transformer that injects the generated endpoints into your OpenAPI specification. MapOpenApiEndpoints() registers the dynamic HTTP endpoints at runtime.
Each REST endpoint is defined by a GraphQL operation annotated with an @http directive. You provide these operations through an IOpenApiDefinitionStorage implementation.
A GET endpoint that fetches a user by ID:
"Fetches a user by their id"
query GetUserById($userId: ID!)
@http(method: GET, route: "/users/{userId}") {
userById(id: $userId) {
id
name
email
}
}
A POST endpoint that creates a user:
"Creates a user"
mutation CreateUser($user: UserInput! @body)
@http(method: POST, route: "/users") {
createUser(user: $user) {
id
name
email
}
}
The @http directive specifies the HTTP method and route. Route parameters like {userId} map to GraphQL variables. The @body directive on a variable indicates that the HTTP request body maps to that variable.
The adapter translates between REST and GraphQL concepts:
| REST Concept | GraphQL Concept |
|---|---|
| HTTP method (GET, POST, PUT) | Specified by @http(method: ...) |
| Route path | @http(route: "/path/{param}") |
| Route parameters | GraphQL variables matched by name |
| Query parameters | Variables listed in queryParameters |
| Request body | Variable annotated with @body |
| Response body | Selected fields from the operation |
When a client sends an HTTP request to a generated endpoint, the adapter extracts route parameters, query parameters, and the request body, maps them to GraphQL variables, executes the operation, and returns the root field's data as the response body.
Route parameters in curly braces map to GraphQL variables by name:
query GetUser($userId: ID!) @http(method: GET, route: "/users/{userId}") {
userById(id: $userId) {
id
name
}
}
A request to GET /users/42 sets $userId to "42".
You can also map route parameters to nested fields of a variable using the key:$variable.path syntax:
mutation UpdateUser($user: UserInput! @body)
@http(method: PUT, route: "/users/{userId:$user.id}") {
updateUser(user: $user) {
id
name
}
}
A PUT request to /users/42 with a JSON body sets the id field of the $user variable to "42", and the rest of the body fills in the remaining fields.
Use the queryParameters argument on the @http directive to expose GraphQL variables as URL query parameters:
query GetUserDetails($userId: ID!, $includeAddress: Boolean!)
@http(
method: GET
route: "/users/{userId}/details"
queryParameters: ["includeAddress"]
) {
userById(id: $userId) {
id
name
address @include(if: $includeAddress) {
street
}
}
}
A request to GET /users/1/details?includeAddress=true sets $includeAddress to true.
Query parameters support the same key:$variable.path mapping syntax as route parameters.
The @body directive on a variable maps the entire HTTP request body to that variable:
mutation CreateUser($user: UserInput! @body)
@http(method: POST, route: "/users") {
createUser(user: $user) {
id
name
email
}
}
A POST request with a JSON body {"id": "6", "name": "Alice", "email": "[email protected]"} sets $user to that object. The request must have a Content-Type: application/json header.
You can define reusable GraphQL fragments as separate documents. The adapter resolves fragment references across documents:
-- Document 1: endpoint definition
query GetUser($userId: ID!)
@http(method: GET, route: "/users/{userId}") {
userById(id: $userId) {
...UserFields
}
}
-- Document 2: shared fragment
fragment UserFields on User {
id
name
email
address {
...AddressFields
}
}
-- Document 3: another shared fragment
fragment AddressFields on Address {
street
}
Each document is a separate entry in your IOpenApiDefinitionStorage. Fragment-only documents are treated as shared models.
The IOpenApiDefinitionStorage interface provides endpoint and fragment definitions to the adapter:
// Services/MyOpenApiStorage.cs
using HotChocolate.Adapters.OpenApi;
using HotChocolate.Adapters.OpenApi.Storage;
using HotChocolate.Language;
public class MyOpenApiStorage : IOpenApiDefinitionStorage
{
public ValueTask<IEnumerable<IOpenApiDefinition>>
GetDefinitionsAsync(
CancellationToken cancellationToken = default)
{
var documents = new List<IOpenApiDefinition>();
var getUserDoc = Utf8GraphQLParser.Parse(
"""
query GetUser($userId: ID!)
@http(method: GET, route: "/users/{userId}") {
userById(id: $userId) {
id
name
}
}
""");
documents.Add(OpenApiDefinitionParser.Parse(getUserDoc));
return ValueTask.FromResult<IEnumerable<IOpenApiDefinition>>(
documents);
}
// IOpenApiDefinitionStorage also extends IObservable, enabling
// hot-reload when definitions change.
public IDisposable Subscribe(
IObserver<OpenApiDefinitionStorageEventArgs> observer)
=> /* your subscription logic */;
}
Register it with your GraphQL server:
// Program.cs
var storage = new MyOpenApiStorage();
builder
.AddGraphQL()
.AddQueryType<Query>()
.AddOpenApiDefinitionStorage(storage);
The storage implements IObservable<OpenApiDefinitionStorageEventArgs>. When you push Updated or Removed events through this observable, the adapter picks up changes at runtime, adding, updating, or removing HTTP endpoints without a restart. This hot-reload behavior extends to the OpenAPI specification.
The adapter integrates with ASP.NET Core's built-in OpenAPI support. After you register AddOpenApi(options => options.AddGraphQLTransformer()), the generated endpoints appear in the OpenAPI document at /openapi/v1.json.
Each endpoint definition's description becomes the OpenAPI operation summary. Route and query parameters become OpenAPI parameters with types inferred from the GraphQL schema. Request body schemas are generated from the GraphQL input types.
The OpenAPI adapter works with Fusion gateway servers. Replace AddGraphQL() with AddGraphQLGateway() and the rest of the configuration remains the same:
// Program.cs
builder
.AddGraphQLGateway()
.AddInMemoryConfiguration(compositeSchema)
.AddHttpClientConfiguration("Subgraph", subgraphUri)
.AddOpenApiDefinitionStorage(myStorage);
The Fusion gateway composes schemas from multiple subgraphs. The OpenAPI adapter generates REST endpoints that execute operations against the composed schema, so a single REST endpoint can fetch data from multiple subgraphs transparently.