sisense / graphql2rest

GraphQL to REST converter: automatically generate a RESTful API from your existing GraphQL API
MIT License
322 stars 36 forks source link
api api-gateway api-rest api-server api-wrapper express-router graphql graphql-api graphql-js graphql-schema graphql-server graphql-to-rest graphql-tools rest rest-api restful restful-api restify

GraphQL2REST

Automatically generate a RESTful API from your existing GraphQL API

Sisense-open-source License: MIT npm-image Tweet

GraphQL2REST is a Node.js library that reads your GraphQL schema and a user-provided manifest file and automatically generates an Express router with fully RESTful HTTP routes — a full-fledged REST API.

Author: Roy Mor

Why?

GraphQL2REST allows you to fully configure and customize your REST API, which may sit on top of a very different GraphQL layer (see features).


Table of Contents

Installation

npm:

npm i graphql2rest

yarn:

yarn add graphql2rest

Usage

Basic example:

Given a simple GraphQL schema:

type Query {
    getUser(userId: UUID!): User
}

type Mutation {
    createUser(name: String!, userData: UserDataInput): User
    removeUser(userId: UUID!): Boolean
}

Add REST endpoints to the manifest.json file:

{
    "endpoints": {
        "/users/:userId": {
            "get": {
                "operation": "getUser"
            },
            "delete": {
                "operation": "removeUser",
                "successStatusCode": 202
            }
        },
        "/users": {
            "post": {
                "operation": "createUser",
                "successStatusCode": 201
            }
        }
    }
}

In your code:

import GraphQL2REST from 'graphql2rest';
import { execute } from 'graphql'; // or any GraphQL execute function (assumes apollo-link by default)
import { schema } from './myGraphQLSchema.js'; 

const gqlGeneratorOutputFolder = path.resolve(__dirname, './gqlFilesFolder'); 
const manifestFile = path.resolve(__dirname, './manifest.json');

GraphQL2REST.generateGqlQueryFiles(schema, gqlGeneratorOutputFolder); // a one time pre-processing step

const restRouter = GraphQL2REST.init(schema, execute, { gqlGeneratorOutputFolder, manifestFile });

// restRouter now has our REST API attached
const app = express();
app.use('/api', restRouter);

(Actual route prefix, file paths etc should be set first via options object or in config/defaults.json)

Resulting API:

POST /api/users              --> 201 CREATED
GET /api/users/{userId}      --> 200 OK
DELETE /api/users/{userId}   --> 202 ACCEPTED

// Example:

GET /api/users/1234?fields=name,role

Will invoke getUser query and return only 'name' and 'role' fields in the REST response.
The name of the filter query param ("fields" here) can be changed via configuration. 

For more examples and usage, please refer to the Tutorial.


Features

How GraphQL2REST works

GraphQL2REST exposes two public functions:

First, GraphQL2REST needs to do some one-time preprocessing. It reads your GraphQL schema and generates .gql files containing all client operations (queries and mutations). These are "fully-exploded" GraphQL client queries which expand all fields in all nesting levels and all possible variables, per each Query or Mutation type.

This is achieved by running the generateGqlQueryFiles() function:

GraphQL2REST.generateGqlQueryFiles(schema, '/gqlFilesFolder');

Now the /gqlFilesFolder contains an index.js file and subfolders for queries and mutations, containing .gql files corresponding to GraphQL operations. Use path.resolve(__dirname, <PATH>) for relative paths.

generateGqlQueryFiles() has to be executed just once, or when the GraphQL schema changes (it can be executed offline by a separate script or at "build time").


After generateGqlQueryFiles() has been executed once, GraphQL2REST init() can be invoked to create REST endpoints dynamically at runtime.

init() loads all .gql files into memory, reads the manifest.json file and uses Express router to generate REST endpoint routes associated with the GraphQL operations and rules defines in the manifest. init() returns an Express router mounted with all REST API endpoints.


The init() function

GraphQL2REST.init() is the entry point that creates REST routes at runtime.

It only takes two mandatory parameters: your GraphQL schema and the GraphQL server execute function (whatever your specific GraphQL server implementation provides, or an Apollo Link function).

GraphQL2REST.init(
    schema: GraphQLSchema,
    executeFn: Function,

    options?: Object,
    formatErrorFn?: Function,
    formatDataFn?: Function,
    expressRouter?: Function)


GraphQL arguments are passed to executeFn() in Apollo Link/fetch style, meaning one object as argument: { query, variables, context, operationName }.

options defines various settings (see below). If undefined, default values will be used.

formatErrorFn is an optional function to custom format GraphQL error responses.

formatDataFn is an optional function to custom format non-error GraphQL responses (data). If not provided, default behavior is to strip the encapsulating 'data:' property and the name of the GraphQL operation, and omit the 'errors' array from successful responses.

expressRouter is an express.Router() instance to attach new routes on. If not provided, a new Express instance will be returned.

The Manifest File

REST API endpoints and their behavior are defined in the manifest file (normally manifest.json ). It is used to map HTTP REST routes to GraphQL operations and define error code mappings. See a full example here.

The endpoints section

The endpoints object lists the REST endpoints to generate:

"endpoints": {
    "/tweets/:id": {  // <--- HTTP route path; path parameters in Express notation
        "get": {      // <--- HTTP method (get, post, patch, put, delete)
            "operation": "getTweetById", // <--- name of GraphQL query or mutation
        }
    }
}

Route path, HTTP method and operation name are mandatory.

GraphQL2REST lets you map a single REST endpoint to multiple GraphQL operations by using an array of operations (operations[] array instead of the operation field).

Additional optional fields:

// Mutation updateUserData(userOid: UUID!, newData: userDataInput!): User
// input userDataInput { name: String, birthday: Date }
// type User { id: UUID!, name: String, birthday: Date, internalSecret: String }

"endpoints": {
    "/users/:id": {
        "patch": {
            "operation": "updateUserData",
            "params": {   // <-- map or rename some params
                "userOid": "id" // <-- value of :id will be passed to userOid in mutation
            },
            "successStatusCode": 202  // <-- customize success status code (202 is strange here but valid)
            "wrapRequestBodyWith": "newData", // <-- allow flat REST request body
            "hide": ["internalSecret"] // <-- array of fields to omit from final REST response
        }
    }
}
// PATCH /users/{userId}, body = {"name": "Joe", "birthday": "1990-1-14"}
// Response: 202 ACCEPTED
// { "id": THE_USERID, "name": "Joe", "birthday": "1990-1-14"} // "internalSecret" omitted

The errors section

The optional “errors” object lets you map GraphQL error codes to HTTP status codes, and add an optional additional error message. The first error element in GraphQL's errors array is used for this mapping.

Example:

"errors": {
    "errorCodes": {
        "UNAUTHENTICATED": {
            "httpCode": 401,
            "errorDescription": "Forbidden: Unauthorized access",
        }
    }
}

In this example, responses from GraphQL that have an errors[0].extension.code field with the value "UNAUTHENTICATED" produce a 401 Unauthorized HTTP status code, and the error description string above is included in the JSON response sent by the REST router.

For GraphQL error codes that have no mappings (or if the "errors" object is missing from manifest.json), a 400 Bad Request HTTP status code is returned by default for client errors, and a 500 Internal Server Error is returned for errors in the server or uncaught exceptions.

Configuration

Settings can be configured in the options object provided to init(). For any fields not specified in the options object, or if options is not provided to init(), values from the config/defaults.json file are used.

const gql2restOptions  = {
    apiPrefix: '/api/v2', //sets the API base path url
    manifestFile: './api-v2-manifest.json', //pathname of manifest file
    gqlGeneratorOutputFolder: './gqls', //.gql files folder (generated by generateGqlQueryFiles())
    middlewaresFile:  './middlewares.js', //optional middlewares module for modifying requests
    filterFieldName: 'fields', //global query parameter name for filtering (default is 'fields'),
    graphqlErrorCodeObjPath: 'errors[0].extensions.code', //property for GraphQL error code upon error
    logger: myCustomLogger //optional Winston-based logger function
};

const expressRouter = GraphQL2REST.init(schema, execute, gql2restOptions);

Use path.resolve(__dirname, <PATH>) for relative paths.

All fields in options are optional, but init() will not be able to run without a valid manifest file and gqlGeneratorOutputFolder previously populated by generateGqlQueryFiles().

The depth limit of generated client queries can be set in the pre-processing step. This might be needed for very large schemas, when the schema has circular references or the GraphQL server has a strict query depth limit.

Tutorial

Running tests

npm test

Or, for tests with coverage:

npm run test:coverage

Benefits

Limitations and Known Issues

• No support for subscriptions yet – only queries and mutations

Acknowledgments

Contact

For inquiries contact author Roy Mor (roy.mor.email at gmail.com).

Release History

Contributing

See CONTRIBUTING.md

License

Distributed under MIT License. See LICENSE for more information.

(c) Copyright 2020 Sisense Ltd