GraphQL powered RESTful API

Written by William TabiSenior Software Engineer, Tines

At Tines, we have a REST API that allows users to perform operations such as creating stories on their tenants. Typically, we've left this REST API unchanged as we built features like Folders and Teams into the product. This limited the power of these new features, as customers were unable to access them through the REST API.

The REST API lagged behind the Web app because each used a completely different method of fetching and updating data, and both of these had to be developed and maintained separately.

The web app's approach - GraphQL 

GraphQL is a query language for building APIs. We use GraphQL extensively at Tines to power the entire front-end of our Web app. We use queries to fetch data from the database for the front-end and mutations to modify data on the server-side.

We sought to unify the access path for both the REST API and the Web app. We found that the best way to do this was to use the existing GraphQL API implementation to power both the Web App and the REST API.

There are a few advantages of doing this, which include:

  • Implement features once: features implemented will be immediately available to the REST API

  • Leverage extensive testing we have on both the front-end and backend on all GraphQL queries and mutations.

  • We get request parameter validations for free through built-in GraphQL type validation

  • We get native support for versioning with GraphQL

You might also ask the question: why didn't we just expose a GraphQL API? This is something we might do in the future, for now, we found that REST is far more accessible to most users, whereas GraphQL has a steeper learning curve. We might also consider releasing a GraphQL API alongside a REST API similar to how other platforms like Github already do.

How we did it 

We set a simple goal to create a pattern for easily creating new REST API endpoints.

We created a declarative syntax for describing a REST API endpoint, meaning that adding an endpoint involves just two steps:

  1. Define your REST API controller action and GraphQL query that should be executed for that action.

  2. Define how the GraphQL response should be transformed to get the data that will be returned to the REST API

Defining an API endpoint 

# /api/v1/my_controller.rb

class API::V1::UsersController < API::V1::GraphQLController
  ROUTES = {
    action_name: {
      query: ->(params, context) {}, # build the GraphQL query to execute
      response: -> (result) {}, # build the API response body
    },
  }
end

e.g This is the definition of our recently released Teams creation REST API endpoint.

# app/controllers/api/v1/teams_controller.rb

class API::V1::TeamsController < API::V1::GraphQLController
  ROUTES = {
        # POST /api/v1/teams
    create: {
      query: ->(params, _context) do
        {
          query: <<~GRAPHQL
            mutation($inputs: TeamCreationInputs!) {
              teamCreation(inputs: $inputs) {
                team {
                  id
                  name
                }
              }
            }
            GRAPHQL,
          variables: { inputs: params }
        }
      end,
      response: ->(result) { result["team_creation", "team"] },
    }
  }
end

So with the configuration above, we can generate a corresponding Rails controller method which will:

  • Parse the request to the REST API endpoint

  • Build a GraphQL query with the parsed request payload

  • Execute the GraphQL query (In our case, since we use the graphql-ruby gem, we simply call TinesSchema.execute with a query built with the REST API request parameters)

  • Translate the GraphQL response into the right format for our REST API

# POST /api/v1/teams
def create
  # Build a GraphQL Query
  query =  <<~GRAPHQL
      mutation($inputs: TeamCreationInputs!) {
        teamCreation(inputs: $inputs) {
          team {
            id
            name
          }
        }
      }
      GRAPHQL
  variables = { inputs: params }

  # Execute the GraphQL query
  result =  TinesSchema.execute(
              query,
              variables: camelize_keys(variables),
              context: {
                current_user: current_user,
              },
            )

  result_hash = result.to_hash

  # Convert GraphQL response into REST API response format
  # - snake-case keys `userName -> user_name`
  # - convert graphql ID into ActiveRecord Id `id: "VXNlci0zMA==" -> id: "1"`
  response = transform_graphql_response_into_api_response(result_hash["data"])

  # 4. Render the response
  render json: response["team_creation", "team"], status: 201
end

Conclusion 

By using our existing GraphQL implementation, we were able to define a clear pattern for creating and updating REST API endpoints. We've achieved feature parity between the Web App and the REST API. This also serves as a base for future work on the API, such as implementing versioning, cursor-based pagination and possibly a public GraphQL API.

If you would like to work on problems like this, we're hiring.

Built by you, powered by Tines

Talk to one of our experts to learn the unique ways your business can leverage Tines.