GraphQL2REST: Automatically generate a RESTful API from your existing GraphQL API
You should read this if you:
1. Already have a GraphQL API, but your clients or partners want REST
2. Want to develop a new GraphQL API and get REST on top of it, for free
3. Want to benefit from GraphQL internally while exposing REST externally as a public API
4. Like your RESTful API to be truly RESTful, on top of a possibly completely different native GraphQL implementation
No, this is not yet another blog post praising GraphQL.
Indeed, GraphQL technology that was publicly introduced in 2015 quickly took over the world of API development, and has some clear benefits over RESTful APIs: a robust query language and type system that lets the API client specify exactly what data it needs and makes it easier to aggregate data from multiple sources using resolvers.
GraphQL APIs help avoid both under-fetching and over-fetching data, provide built-in data validation, and are “self-documenting,” among other things.
On the other hand, REST has been around forever: it is the de facto standard of web APIs and has a super high adoption rate with well known conventions. Many scenarios call for a REST API — so it’s a matter of selecting the right tool for the job.
At Sisense, we strive to adopt the newest and best technologies and pick the right tool for each job. So when we designed our ElastiCube Manager (ECM) service for Sisense web application, we chose GraphQL as our internal API layer, implemented on both the server side and on the client side.
A question for you
Time for a philosophical question: once you commit to a new technology, does that mean you HAVE to forsake the old one?
OR: If you had to write a new public API today, which technology would you use? RESTful APIs? GraphQL?
Why not have both?
In our case, we already had an internal GraphQL API running our web application, and we wanted to provide our customers with an open, public API into our ElastiCube Manager server.
The natural step was to just expose a subset of that existing GraphQL API publicly, since it provides exactly the functionality we’d like to give our customers. However, many of our customers were already accustomed to REST APIs in our previous versions, and demanded REST!
In your case, perhaps you’d like to get all the benefits of GraphQL technology, but know you might also need to provide a RESTful interface for some users; or you’d like to migrate from REST to GraphQL, but need to do it gently and gradually; or you simply have a requirement for exposing BOTH protocols, whether internally or externally.
It’s just not cost-effective to develop, test and maintain two code paths with the same functionality, and keep them in parity over time.
Wouldn’t it be great if there was a tool that could automatically generate a fully RESTful API from your GraphQL schema?
We thought so too. But we couldn’t find one.
So we created GraphQL2REST .
GraphQL2REST is a Node.js library that reads your GraphQL schema and a manifest file, and automatically generates an Express router with fully RESTful HTTP routes — a full-fledged REST API.
GraphQL2REST can be installed on your GraphQL server or on a remote server, and creates REST endpoints dynamically at runtime. It lets you fully configure and customize your REST API, which may sit on top of a very different existing GraphQL layer.
GraphQL2REST features:
- Use any type of GraphQL server — you provide the execute() function
- Default “RESTful” logic for error identification, determining status codes and response formatting
- Customize response format (and error responses format too, separately)
- Custom parameter mapping (REST params can be different than GraphQL parameter names)
- Customize success status codes for each REST endpoint
- Map custom GraphQL error codes to HTTP response status codes
- Map a single REST endpoint to multiple GraphQL operations, with conditional logic to determine mapping
- Hide specific fields from responses
- Run custom middleware function on incoming requests before they are sent to GraphQL
- A built-in filter parameter in all REST endpoints lets the client filter on all fields of a response
- Built-in JMESPath support (JSON query language) for client filter queries
- GraphQL server can be local or remote (supports apollo-link and fetch to forward request to a remote server)
- Embed your own winston-based logger
You have to start from the REST side
When we first developed GraphQL2REST, we created an early “simple mode” version that read the GraphQL schema and automatically generated an API endpoint per GraphQL operation — GET endpoints for queries and POST endpoints for mutations. It was a simple mapping from GraphQL operations to REST API routes. An options object was then used to customize some of the routes — changing the HTTP method and URL path of REST endpoints, for example.
However we soon learned that this approach will not work for most real-world cases in production, where the REST API is required to be truly RESTful and yet the GraphQL layer needs to be left unchanged.
Since a REST API models itself around resources and methods (verbs), one endpoint could be mapped to multiple existing GraphQL operations (the actual one invoked at runtime is determined according to the contents of the payload). And there are usually other significant gaps between the spec of a RESTful API and the existing GraphQL API, that was never designed with the notion of “resources” and “methods” in mind.
This showed us that our design has to treat the desired REST API as the “master blueprint”, and create the mapping from REST routes into the existing GraphQL operations.
GraphQL2REST uses a manifest file to create your REST API
We start by creating a mapping from our desired REST API routes to the GraphQL operations (queries and mutations).
We get to act as API designers and design the API from the outside in.
Because we now start “thinking in REST first,” there is no automatic mapping from GraphQL to REST, and we have to provide many of the details ourselves. However, we get a more expressive and powerful framework for specifying our route definitions, which will be generated automatically by GraphQL2REST.
These definitions are specified in the manifest.json file which include route names (with path parameters in Express notation), HTTP methods, parameter mapping, HTTP status codes, fields to hide and so on.
Let’s assume our GraphQL schema includes these operations:
type Query { getUserObjByOid(userOid: UUID!): User listAllDatasets: [Dataset] }type Mutation { removeUser(userOid: UUID!): Boolean addDatasetV1ToCollection(datasetToAdd: Dataset!): Boolean addDatasetV2ToCollection(newDatasetFormat: DatasetV2!): Boolean }
Where addDataSetV1ToCollection() and addDatasetV2ToCollection() are two versions of an operation providing similar functionality.
An example manifest.json for generating a RESTful API will look like this:
{ "endpoints": { "/users/:id": { "get": { "description": "Get user by ID", "operation": "getUserObjByOid", "params": { "userOid": "id" } }, "delete": { "description": "Delete user by ID", "operation": "removeUser", "params": { "userOid": "id" }, "successStatusCode": 204 } }, "/datasets": { "get": { "description": "Get list of all datasets in the system", "operation": "listAllDatasets" }, "post": { "operations": [{ "description": "Create new dataset instance V1", "operation": "addDatasetV1ToCollection", "condition": { "type": { "$in": ["old", "v1"] }, "params": { "datasetToAdd": "data" }, "successStatusCode": 201, "hide": ["v1_internalRepresentation", "ownerId"] } }, { "description": "Create new dataset instance V2", "operation": "addDatasetV2ToCollection", "condition": { "type": { "$in": ["new", "v2"] }, "params": { "newDatasetFormat": "data" }, "successStatusCode": 201 } } ] } } }, "errors": { "errorCodes": { "1000": { "httpCode": 500 }, "4003": { "httpCode": 403, "errorDescription": "Forbidden: Unauthorized access" }, "1017": { "httpCode": 503, "errorDescription": "Service Unavailable: Cannot retrieve logs, please try again later" } } } }
On the first entry, route GET /users/{id} will be generated and forwarded to the GraphQL query getUserObjByOid(), and the REST path parameter “id” will be mapped to the query’s userOid parameter.
Next, route DELETE /users/{id} will be generated and forwarded to the GraphQL mutation removeUser(), with the same param mapping. Upon successful response (non-error), the endpoint will return “204 No Content” instead of the default “200 OK”.
Next, the /datasets route will be generated with a GET endpoint and a POST endpoint.
For the POST endpoint, we define an array of operations:
In this example, if the endpoint is called with the parameter “type” with the values “old” or “v1” (e.g., POST /datasets?type=v1), the mutation addDatasetV1ToCollection() will be called, mapping the REST (body) parameter “data” to the “datasetToAdd” argument in the mutation.
Upon successful response, the endpoint will return HTTP status code “201 Created”.
The fields “v1_internalRepresentation” and “ownerId” will always be omitted from the response, since they are listed in the “hide” array.
If “condition” is not satisfied, GraphQL2REST moves on to the next operation object. Here there is another, different condition: If the REST endpoint is called with “type=new” or “type=v2”, the mutation addDatasetV2ToCollection() will be called, with a different parameter mapping done from the “data” REST parameter. No field hiding will take place.
(“params” is only used to optionally rename parameters. If it is omitted, or for param names not listed, REST parameters are passed to the GraphQL operation as is.)
The errors object allow mapping GraphQL error codes to HTTP status codes and adding an optional custom errorDescription.
If no such mapping exists, the endpoints return “400 Bad Request” for all client-related errors and “500 Server Error” for all GraphQL server errors by default.
This way, GraphQL2REST allows creating a truly RESTful API that might be very different than the original, unchanged GraphQL API, without writing almost a single line of code!
How it’s used in your code
GraphQL2REST exposes just two public functions: generateGqlQueryFiles()
, a pre-processing function that only has to run once, and init()
, which generates the API routes at runtime and returns an Express router. After editing the manifest file, it only takes a few lines of code to generate the REST API:
The init() function
GraphQL2REST.init() is the entry point which 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 perhaps an Apollo Link function).
In addition, it accepts four optional parameters for further customization. An options object which determines the API base url, parameter name for filter, manifest file location, gql files folder, custom logger function and other settings; functions for formatting the output of both valid and erroneous responses; and an existing Express router instance, to mount the new API routes on.
How GraphQL2REST works
This library is based on two main components. The first one is a a pre-processing module that reads your GraphQL schema, builds the AST tree, 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 component is a wrapper around gql-generator, a node module originally used for API testing. It only needs to be invoked one time (or whenever the GraphQL schema changes), as a pre-processing step.
The other component reads the manifest.json file and uses Express router to generate REST endpoint routes associated with the GraphQL operations and rules defined in the manifest. The Express callback function invoked once the endpoint is called retrieves and executes the appropriate GraphQL client query, populating it with the parameters sent by the user.
When it receives back the response from the GraphQL server, this module determines if the response should be treated as an actual error or as a successful response, applies filters, and formats the response according to defaults or to the user-provided formatting function. It then sends the appropriate status code along with the response back to the HTTP client.
Our implementation choices
In our case, we chose to execute the pre-processing step (.gql file generation) only during service build time, since the GraphQL schema is static. The init() function auto-generating REST routes is executed at runtime upon service start and restart.
An obvious benefit:
The resulting REST API enjoys the built-in data validation provided by GraphQL due to its strong type system. Executing an API call with missing, malformed, or incorrect type parameters will automatically return an informative error message to the user, explaining what needs to be corrected in the API query.
Migrate from an old REST API to GraphQL
An old REST API can be migrated to a new GraphQL API gradually, by first building the GraphQL API and using GraphQL2REST to generate a REST API on top of it, seamlessly. That new REST API will have the same interface as the old one, and the new implementation can then be tested, endpoints migrated in stages, until a full migration to the underlying GraphQL API takes place.
Alternatives
At the time of writing this package, there was no known tool that could do an automatic conversion between GraphQL and REST, which is why I started working on GraphQL2REST. During that time I contacted Uri Goldshtein from The Guild, and we set to collaborate on writing together an open source tool for converting GraphQL to REST based on some of my ideas.
Eventually we had conflicting schedules and priorities, and so I ended up developing GraphQL2REST independently at Sisense. Meanwhile, Uri created SOFA — a cool library for creating REST APIs from GraphQL.
SOFA takes a slightly different approach to converting GraphQL to REST. It supports GraphQL subscriptions (as webhooks) and offers Swagger integration, which are a big plus. However, it can only generate a truly RESTful API if the underlying GraphQL API is already “RESTful”, and it doesn’t offer the flexibility and many of the customization and configuration options that GraphQL2REST offers, letting you create a truly RESTful API with new attributes on top of an unmodified GraphQL API, considering HTTP status codes etc. And so IMHO it might not be a good fit for many real-world use cases. But definitely check it out as it might be suitable for your case!
Where can I get GraphQL2REST?
GraphQL2REST is available as an open source npm package for public use, with source code on Sisense Github repository. Feel free to use and contribute!
Conclusion
GraphQL2REST allows you to get the best of both worlds for free. By just editing a manifest file and adding a few lines of code, a fully customized RESTful API is generated for you on top of GraphQL, to fit your specific use case.
At Sisense, this tool helped us boost time-to-market of our REST API offering, and reduced our costs of development, manual coding, testing and maintenance. Hopefully, it’ll help you too.
GitHub: https://github.com/sisense/graphql2rest
NPM: https://www.npmjs.com/package/graphql2rest
Talk at Node.TLV 2020 Conference:
“GraphQL API? REST API? Or maybe you can have both?” (on YouTube)
This article originally appeared here.