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 APIOAOperation
The 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.
OAParam
The 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.
OABody
Lets 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.
OADescription
You 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:
createHTTPExecutor
GraphQLClient
from
the graphql-request
package (mainly a URL)createSchemaExecutor
GraphQLSchema
from the graphql
package as
argumentAs a second optional argument it takes a configuration object with the properties:
responseTransformer
validateRequest
true
validateResponse
false
responseTransformer
Sometimes 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:
baseSchema
validate
false
The 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