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:
Define your REST API controller action and GraphQL query that should be executed for that action.
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.