The package creates an HTTP API and a corresponding OpenAPI schema based on annotated GraphQL operations.
application/x-www-form-urlencoded requestsmultipart/form-data requests with file uploads as specified in
the graphql-multipart-request-spec specification.With npm:
npm install @freshcells/graphql-rest-converter
With yarn:
yarn add @freshcells/graphql-rest-converter
The package generates an OpenAPI schema for version 3.0 of the specification.
To define a HTTP API request:
The entry point is
the createOpenAPIGraphQLBridge
function.
The function takes a config with:
The function returns an object with two functions:
getExpressMiddleware
is used to get the request handlers for the HTTP API as an express middlewaregetOpenAPISchema
is used to the get the OpenAPI schema for the HTTP APIOAOperationThe OAOperation GraphQL directive is required on an GraphQL operation to map it to an HTTP request.
The arguments of the directive are described by the TS type:
interface OAOperation {
path: string
tags?: [string]
summary?: string
description?: string
externalDocs?: {
url: string
description?: string
}
deprecated: boolean
method: HttpMethod // GET, DELETE, POST, PUT
}
The path argument is required and defines the URL path of the resulting request.
Path parameters can be defined with OpenAPI syntax, for example: /my/api/user/{id}.
The other arguments are mapped directly to the resulting OpenAPI schema for the request.
OAParamThe OAParam is optional, every operation's variable declaration results in an API parameter.
If the path defined in the operation's OAOperation directive contains a parameter matching the variable name, the
variable will be mapped from the path parameter.
Otherwise, the variable will be mapped from a query or header parameter.
The arguments of the directive are described by the TS type:
interface OAParam {
in?: 'path' | 'query' | 'header'
name?: string
description?: string
deprecated?: boolean
}
The in argument can be used to change the type of parameter.
It is useful, for example, if a variable should be mapped from a header instead of a query parameter.
The name argument can be used to explicitly define the parameter name. If it is not provided it uses the variable
name.
It is useful, for example, if the desired parameter name is not a valid GraphQL variable name.
The other arguments are mapped directly to the resulting OpenAPI schema for the parameter.
OABodyLets you mark a query argument to be extracted from the request body.
Mainly designed for input types in combination with a mutation.
interface OABody {
description?: string
path?: string
contentType?: 'JSON' | 'FORM_DATA' | 'MULTIPART_FORM_DATA'
}
You can have multiple arguments annotated with OABody, the variable name (or the path overwrite) will then be
expected as key in the request
body. If you annotate only a single InputType, which is an object, there is no additional hierarchy introduced,
the InputType object is expected in the root.
OADescriptionYou may optionally provide / override descriptions for fragment and field definitions.
interface OADescription {
description: string
}
To generate the request handlers the
function getExpressMiddleware
is used.
As the first argument it takes a GraphQLExecutor implementation:
createHTTPExecutorGraphQLClient from
the graphql-request package (mainly a URL)createSchemaExecutorGraphQLSchema from the graphql package as
argumentAs a second optional argument it takes a configuration object with the properties:
responseTransformer
validateRequesttruevalidateResponsefalseresponseTransformerSometimes it may be necessary to adjust the HTTP response to achieve some required API behavior.
For example:
In general, when making use of this feature the OpenAPI schema needs to be adjusted accordingly.
If a response is transformed it will not be validated even if validateResponse is configured.
If used, a function should be provided. See the API documentation for the type of the function: https://freshcells.github.io/graphql-rest-converter/modules.html#ResponseTransformer.
It will be called for every request to the HTTP API with:
It should return:
undefined if the response should not be customizedTo generate the OpenAPI schema the
function getOpenAPISchema
is used.
As an argument it takes a configuration object with the properties:
baseSchemavalidatefalseThe generated OpenAPI schema contains the customer properties x-graphql-operation and x-graphql-variable-name.
These custom properties contain all necessary information to generate the request handlers.
To remove the custom properties from the OpenAPI schema, for example before serving it publicly, the
transformation
function removeCustomProperties
can be used.
This example showcases most of the usage discussed so far. The hypothetical Star Wars GraphQL schema is inspired by the official GraphQL introduction: https://graphql.org/learn/.
import express from 'express'
import fetch from 'node-fetch'
import { buildClientSchema, getIntrospectionQuery } from 'graphql'
import { gql } from 'graphql-tag'
import {
removeCustomProperties,
transform,
createHttpExecutor,
} from '@freshcells/graphql-rest-converter'
import { createOpenAPIGraphQLBridge } from '@freshcells/graphql-rest-converter/express'
const GRAPHQL_ENDPOINT = 'https://example.org/graphql'
const BRIDGE_DOCUMENT = gql`
query getHeroByEpisode(
$episode: String!
$includeAppearsIn: Boolean! = false
@OAParam(
in: QUERY
name: "include_appears_in"
description: "Include all episodes the hero appears in"
deprecated: false
)
)
@OAOperation(
path: "/hero/{episode}"
tags: ["Star Wars", "Hero"]
summary: "Retrieve heros"
description: "Retrieve heros by episode, optionally including the episodes they appear in"
externalDocs: {
url: "https://www.google.com/search?q=star+wars"
description: "More information"
}
deprecated: false
) {
hero(episode: $episode) {
name
appearsIn @include(if: $includeAppearsIn)
}
}
mutation createANewHero($name: String, $hero: HeroInput! @OABody(description: "Our new Hero"))
@OAOperation(path: "/hero/{name}", tags: ["Star Wars", "Hero"], method: POST) {
createNewHero(name: $name, input: $hero) {
name
}
}
`
const LOCAL_PORT = '3000'
const API_PATH = '/star-wars'
const BASE_OPENAPI_SCHEMA = {
openapi: '3.0.3',
info: {
title: 'Star Wars API',
description: '...',
version: '1.0.0',
},
servers: [
{
url: API_PATH,
description: 'Local server',
},
],
}
const getCustomScalars = (scalarTypeName) => {
return {
DateTime: {
type: 'string',
format: 'date-time',
},
JSON: {},
}[scalarTypeName]
}
async function main() {
const app = express()
const graphqlSchema = buildClientSchema(
(
await (
await fetch(GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: getIntrospectionQuery() }),
})
).json()
).data
)
const openAPIGraphQLBridge = createOpenAPIGraphQLBridge({
graphqlSchema,
graphqlDocument: BRIDGE_DOCUMENT,
customScalars: getCustomScalars,
})
const openAPISchema = openAPIGraphQLBridge.getOpenAPISchema({
baseSchema: BASE_OPENAPI_SCHEMA,
validate: true, // Default is false
// `removeCustomProperties` can be omitted if the underlying GraphQL operations should be visible as custom properties
transform: removeCustomProperties,
// or multiple transformers:
// transform: transform(removeCustomProperties, yourOwnTransformer)
})
const httpExecutor = createHttpExecutor(GRAPHQL_ENDPOINT)
const apiMiddleware = openAPIGraphQLBridge.getExpressMiddleware(httpExecutor, {
validateRequest: true, // Default is true
validateResponse: true, // Default is false
// Optional, can be used for customized status codes for example
responseTransformer: async ({ result, openAPISchema: { operation } }) => {
if (
operation?.operationId === 'getHeroByEpisode' &&
result?.status === 200 &&
!result?.data?.hero?.length
) {
return {
statusCode: 404,
contentType: 'application/json',
data: JSON.stringify({ error: 'No heros found' }),
}
}
},
})
app.use(API_PATH, apiMiddleware)
app.get('/openapi.json', (req, res) => {
res.json(openAPISchema)
})
app.listen(LOCAL_PORT)
}
main()
The library provides out of the box support for express. You may provide your own server with
import { createRequestHandler } from '@freshcells/graphql-rest-converter'
// ...
Please consult the express implementation for an example.
Generated using TypeDoc