Godwin Ekuma I learn so that I can solve problems.

Switching from GraphQL to REST? Take a load off with Sofa

7 min read 2024

Switch GraphQL to REST With Sofa

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.

Why 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.

How Sofa works

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.

Adding an author with GraphQL

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"
    }
  }
}

Adding an author with REST

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"
}

Listing books with GraphQL

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"
          }
        ]
      },

Listing Books with REST

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"
      }
    ]
  }
]

Generating REST documentation

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.

Conclusion

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.


More great articles from LogRocket:


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.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.LogRocket Dashboard Free Trial Bannerhttps://logrocket.com/signup/

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.

Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side
  3. $ 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>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
Godwin Ekuma I learn so that I can solve problems.

One Reply to “Switching from GraphQL to REST? Take a load off…”

  1. The last example /books
    It is not strict restish , right? Because with rest it would be broken into two requests.

Leave a Reply