There are several reasons why you might switch from GraphQL to REST or convert a GraphQL AP to a REST API. For one very basic example, you may want to accommodate API consumers who prefer REST.
GraphQL is not a one-size-fits-all solution. It was designed to declaratively enable you to select only the information or operations you need. This can be a blessing and a curse; asking for too many things can impact your app’s performance.
As your user base grows, you may want to store the content of a request in reverse proxy server to reduce the amount of traffic to a server. Or you might decide to keep frequently accessed information in a location close to the client using a CDN.
A REST API usually exposes numerous endpoints, so it’s easy to configure a web cache to match certain URL patterns, HTTP methods, or specific resources. This is not the case with GraphQ, which exposes only a single endpoint for making queries. Since each query can be different, it is harder to configure web caching.
These are just a few benefits you can enjoy by harnessing the best qualities of both GraphQL and REST. But how do you make the switch in practice?
One method is to reimplement your GraphQL queries using REST endpoints. Another is to implement an HTTP proxy server that accepts REST requests and then calls the GraphQL API before returning a response.
In this tutorial, I’ll walk you through how to use my preferred solution for converting GraphQL queries, mutations, and subscriptions to REST API: Sofa.
Sofa is a Node.js library that you install on your GraphQL server. According to the official docs, Sofa “takes your GraphQL Schema, looks for available queries, mutations and subscriptions and turns all of that into REST API.”
Using Sofa to switch your GraphQL API to REST enables you to get your REST API working in minutes instead of days, weeks, or even months. Sofa simply takes your existing GraphQL schema and returns corresponding REST endpoints, eliminating the need to rewrite your codebase or write a proxy server. In that way, it enables you to gradually migrate to REST without impacting the internal implementation of your existing code. It comes autogenerated and with up-to-date REST API documentation to boot.
Let’s set up a small book and author API server with the help of Sofa, GraphQL, and Express. Then, we’ll create and read books and authors from it.
First, initialize a new Node.js project and install the required dependencies.
mkdir sofa-api-example cd sofa-api-example npm init npm install express typescript graphql express-graphql graphql-tools
Create a TypeScript config file (tsconfig.json
) with the command below.
npx tsc --init --rootDir src --outDir build \ --esModuleInterop --resolveJsonModule --lib es6 \ --module commonjs --allowJs true --noImplicitAny true
Next, create a schema file (types.ts
).
// types.ts export const typeDefs = ` type Book { id: ID! title: String! author: [Author!]! summary: String genre: [Genre] } type Author { id: ID! firstname: String! lastname: String! dob: String } type Genre { id: ID! name: String! } input AuthorInput{ firstname: String! lastname: String! dob: String } input BookInput{ title: String! author: String! summary: String genre: String! } type Query { book(id: ID!): Book books: [Book!] author(id: ID!): Author authors: [Author!] genre(id: ID!): Genre genres: [Genre!] } type Mutation { addBook(book: BookInput!): Book addAuthor(author: AuthorInput!): Author addGenre(name: String!): Genre } type Subscription { onBook: Book } schema { query: Query mutation: Mutation subscription: Subscription } `;
Having defined the schemas, the next step is to define the corresponding resolvers.
// resolver.ts import { books, authors, genres } from './data'; import Chance from 'chance'; const chance = new Chance(); import { PubSub } from 'graphql-subscriptions'; const pubsub = new PubSub(); const BOOK_ADDED = 'BOOK_ADDED'; export const resolvers = { Query: { book(_: any, { id }: any){ return books.find(book => book.id === id) }, books(){ return books }, author(_: any, { id }: any){ return authors.find( author => author.id === id) }, authors(){ return authors; }, genre(_: any, { id }: any){ genres.find( genre => genre.id === id); }, genres(){ return genres } }, Mutation: { addBook(_: any, bookInput: any) { const book = {...bookInput, id: chance.guid()} books.push(book) pubsub.publish(BOOK_ADDED, { onBook: book }); }, addAuthor(_: any, authorInput: any) { const author = {...authorInput, id: chance.guid()}; authors.push(author); return author; }, addGenre(_: any, name: string) { const genre = {name, id: chance.guid()}; genres.push(genre); return genre; }, }, Subscription: { onBook: { subscribe: () => pubsub.asyncIterator([BOOK_ADDED]), }, }, }
Now that we have the schema and the resolver, let’s wire up the REST and GraphQL APIs.
// index.ts import { makeExecutableSchema } from '@graphql-tools/schema'; import express from 'express'; import bodyParser from 'body-parser'; import { graphqlHTTP } from 'express-graphql'; import { typeDefs } from './types'; import { resolvers } from './resovers' import { useSofa, OpenAPI } from 'sofa-api'; import * as swaggerDocument from './swagger.json'; import * as path from 'path'; import * as swaggerUi from 'swagger-ui-express'; const app = express(); app.use(bodyParser.json()); const schema = makeExecutableSchema({ typeDefs, resolvers, }); app.use('/api', useSofa({ schema, }) ); app.use( '/graphql', graphqlHTTP({ schema, graphiql: true, }) ); const port = 4000; app.listen(port, () => { console.log(`started server on port: ${port}`) });
In the above file, we used makeExecutableSchema
from the @graphql-tools/schema
module to combine the type definitions and resolvers. Next, we created two separate APIs. The first is the REST API that we created using useSofa
middleware, which accepts schema
as an argument. It exposes REST APIs via the /api
route. The second is the GraphQL API, which is exposed via the /graphql
route. The GraphQL API has the GraphQL UI enabled.
Sofa converts all GraphQL queries to GET
endpoints, mutations to POST
, and subscriptions to webhooks. It’s also possible to customize the HTTP verb to be used for a specific query or mutation. For example, if you need a PUT
instead of POST
method in one of your mutations, you can do the following.
api.use( '/api', sofa({ schema, method: { 'Mutation.addGenre': 'PUT', }, }) );
Now let’s test some of the GraphQL queries and mutations and their corresponding REST endpoints.
Request:
mutation{ addAuthor(author: {firstname: "John", lastname: "Doe", dob:"2020-08-15"}){ id firstname lastname dob } }
Response:
{ "data": { "addAuthor": { "id": "cd9aada0-2c59-5f5a-9255-7835ecd19d76", "firstname": "John", "lastname": "Doe", "dob": "2020-08-15" } } }
Request:
curl --header "Content-Type: application/json" \ --request POST \ --data '{"author":{"firstname": "John", "lastname": "Doe", "dob":"2020-08-15"}}' \ http://localhost:4000/api/add-author {"id":"fd8e1958-cc1f-52b4-8bc1-53710616fd0d","firstname":"John","lastname":"Doe","dob":"2020-08-15"}%
Response:
{ "id": "fd8e1958-cc1f-52b4-8bc1-53710616fd0d", "firstname": "John", "lastname": "Doe", "dob": "2020-08-15" }
Request:
{ books{ id title summary genre{ name } author{ firstname lastname } } }
Response:
{ "data": { "books": [ { "id": "b2ca39a8-e21b-547a-9da4-eff9e0f6e113", "title": "Di rujen fug nebitovo dodmikut.", "summary": "Za lo zenle mibajfem icudip zezucvod gun vuwtait nu mod asamockin obu ewubub zodez roragu.", "genre": [ { "name": "ohva" }, { "name": "hohutten" } ], "author": [ { "firstname": "Eunice", "lastname": "Hunter" } ] }, { "id": "d2075892-e44b-5a5c-ac75-62d5639655b1", "title": "Neti ud ciribnoc re ukse.", "summary": "Mazraz zoc maprogna gikmef se ve joge wavca vawavo upkeep hiut madtadla fude uwka lepekjij igimav.", "genre": [ { "name": "ohva" }, { "name": "dif" } ], "author": [ { "firstname": "Steven", "lastname": "Fred" } ] },
Request:
curl --header "Content-Type: application/json" \ --request GET \ http://localhost:4000/api/books
Response:
[ { "id": "b2ca39a8-e21b-547a-9da4-eff9e0f6e113", "title": "Di rujen fug nebitovo dodmikut.", "author": [ { "id": "fc118537-2cc8-558c-abb6-0733bf1ddfd1" } ], "summary": "Za lo zenle mibajfem icudip zezucvod gun vuwtait nu mod asamockin obu ewubub zodez roragu.", "genre": [ { "id": "6ad4d748-bf88-5a89-8ca0-d73e8de3ed18" }, { "id": "492b4ae9-1c07-5f6f-b5a6-9258d24338e1" } ] }, { "id": "d2075892-e44b-5a5c-ac75-62d5639655b1", "title": "Neti ud ciribnoc re ukse.", "author": [ { "id": "31cbd90d-73a4-5649-a0ce-ad230f41e2f8" } ], "summary": "Mazraz zoc maprogna gikmef se ve joge wavca vawavo upkeep hiut madtadla fude uwka lepekjij igimav.", "genre": [ { "id": "6ad4d748-bf88-5a89-8ca0-d73e8de3ed18" }, { "id": "ff85e7bb-37bc-5875-9243-0b7fec42b286" } ] }, { "id": "aafc2536-ef57-503a-bf18-309cdad3a835", "title": "Et urvowpi josrowus wervek wuce.", "author": [ { "id": "fc118537-2cc8-558c-abb6-0733bf1ddfd1" } ], "summary": "Hoot ez poifufo hal urlirguw irpomol sozca zok agloh ak ra ovves kidme.", "genre": [ { "id": "6ad4d748-bf88-5a89-8ca0-d73e8de3ed18" }, { "id": "ff85e7bb-37bc-5875-9243-0b7fec42b286" } ] }, { "id": "a6152ed4-430f-55cd-b750-ca5bac562640", "title": "Lofe melrazlov tu zu ra.", "author": [ { "id": "fc118537-2cc8-558c-abb6-0733bf1ddfd1" } ], "summary": "Vibaduf nagad ocele rigo nirjo ermosno fu det cuh fa hej bopozbo hasna cufif monapmok ubaulewol luru.", "genre": [ { "id": "492b4ae9-1c07-5f6f-b5a6-9258d24338e1" }, { "id": "ff85e7bb-37bc-5875-9243-0b7fec42b286" } ] }, { "id": "a5d9a306-edfa-5564-8c88-0f27ed7d1742", "title": "Ehinaj sowum ezufokew amwemah ifumuc.", "author": [ { "id": "31cbd90d-73a4-5649-a0ce-ad230f41e2f8" } ], "summary": "Guvek mab itaanubo gogogsar duva pidi vu ropvum luvud hubguz lille odro dadespe suafaka sos.", "genre": [ { "id": "6ad4d748-bf88-5a89-8ca0-d73e8de3ed18" }, { "id": "ff85e7bb-37bc-5875-9243-0b7fec42b286" } ] }, { "id": "8f507b93-a2c1-54c8-b660-0b40c411480c", "title": "Elihin lottev ew bi dernoza.", "author": [ { "id": "8989180f-6b7b-5bc2-a367-fcd9b816ed26" } ], "summary": "Vo tazipnep ire joucamu uhjomet ebubekaja eziwenhib piw gatcokup keijsec uculive kajes hehud uv lano.", "genre": [ { "id": "ff85e7bb-37bc-5875-9243-0b7fec42b286" }, { "id": "85d3ee83-7594-5c8c-85c4-c33233e4323c" } ] }, { "id": "8cf2e033-6823-56de-9424-bc4072c464e3", "title": "Jeztoz jisnifa worazet kanpede ti.", "author": [ { "id": "8989180f-6b7b-5bc2-a367-fcd9b816ed26" } ], "summary": "Fu tazoj socdigzo hanemnep li da bopacfow lugic nam onekaam og ezurni ku liiz ce ha.", "genre": [ { "id": "85d3ee83-7594-5c8c-85c4-c33233e4323c" }, { "id": "6ad4d748-bf88-5a89-8ca0-d73e8de3ed18" } ] }, { "id": "1b57d182-a083-589b-845d-03770c22f08f", "title": "Waj vudsadso ju umameto nokojjuk.", "author": [ { "id": "992b2ec7-cd79-5a22-b0e7-d9fba294456d" } ], "summary": "Bi do ipi riwwifel wugaz fekel tejaak wot vudlavgo hasir giti paj soprakju.", "genre": [ { "id": "85d3ee83-7594-5c8c-85c4-c33233e4323c" }, { "id": "6ad4d748-bf88-5a89-8ca0-d73e8de3ed18" } ] }, { "id": "0f348d87-15db-53e4-943a-925ba93ce806", "title": "Le da tiorloj nansuzve jeesosak.", "author": [ { "id": "31cbd90d-73a4-5649-a0ce-ad230f41e2f8" } ], "summary": "Doowam cu tepaluj buv cer danorma sebovo obusoc ne nu hojefiw puov muhogre oke kucjuzpev tacet cuto kimab.", "genre": [ { "id": "492b4ae9-1c07-5f6f-b5a6-9258d24338e1" }, { "id": "ff85e7bb-37bc-5875-9243-0b7fec42b286" } ] }, { "id": "d48cfe82-5e26-59de-9025-cdf19b4461a9", "title": "Ok izu udihap necfisa di.", "author": [ { "id": "8989180f-6b7b-5bc2-a367-fcd9b816ed26" } ], "summary": "Re rueh wawule raigomo vijteco oso ceva tuh hup talac popozude zahatu.", "genre": [ { "id": "ff85e7bb-37bc-5875-9243-0b7fec42b286" }, { "id": "492b4ae9-1c07-5f6f-b5a6-9258d24338e1" } ] } ]
Sofa is able to generate OpenAPI documentation using the schema definition file. To autogenerate the documentation for our author API, we would extend the Sofa middleware with the onRoute
option.
// index.ts app.use('/api', useSofa({ schema, onRoute(info) { openApi.addRoute(info, { basePath: '/api', }); }, }) ); openApi.save(path.join(__dirname, '/swagger.yml')); openApi.save(path.join(__dirname, '/swagger.json')); app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
The documentation is served via the /api/docs
route using swagger-ui-express
middleware.
Sofa takes advantage of GraphQL’s standardized schemas and resolvers to map certain API concepts back to REST. It is designed to help you speed up migration to or support of REST in your API and provide your users with different API types.
I would personally recommend Sofa because it eliminates the need to write new controllers and endpoints or even document your API.
The complete code for this post is available on GitHub.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Switching from GraphQL to REST? Take a load off with Sofa"
The last example /books
It is not strict restish , right? Because with rest it would be broken into two requests.