Proof of concept for a GraphQL wrapper around existing REST API (in this case, Dropbox). GraphQL queries are decomposed into corresponding REST API requests.
The GraphQL-to-REST-API decomposition can happen:
- Purely on the client in the browser (without need for a GraphQL server)
- Purely on the server (as one GraphQL request)
- As a mix of the two for the same query (executing some resolvers on the server and the rest on the client)
This approach reuses the same resolver code on the client and server. You don't need to implement the GraphQL-to-REST-API decomposition logic twice.
You can also use feature gating to extend #3 to gradually transition use of a GraphQL resolver from the client to server.
More on both of these use cases toward the end of the README.
This example uses:
apollo-client
to run all GraphQL queries in the web clientapollo-link-state
to run resolvers locally in the clientapollo-server
to run the GraphQL serverreact
andreact-apollo
to render UX on the web clientdropbox
to run Dropbox REST API calls
This POC wraps three Dropbox API calls in a GraphQL schema. It's not intended to be a full GraphQL implementation of the Dropbox API:
common-src/schema.mjs:
type Query {
filesListFolder(path: String): [FileEntry!]!
}
type Mutation {
filesMove(fromPath: String!, toPath: String!): FileEntry
}
type FileEntry {
id: ID!
name: String!
path_display: String!
revisions: [FileRevision!]!
tag: String!
}
type FileRevision {
id: ID!
server_modified: String!
temporaryDownloadLink: String!
}
- The
filesListFolder
query resolver wraps Dropbox's /files/list_folder REST endpoint - The
filesMove
mutation resolver wraps Dropbox's /files/move (v2) - The
FileEntry.revisions
resolver wraps Dropbox's /files/list_revisions REST endpoint - The
FileRevision.temporaryDownloadLink
resolver wraps Dropbox's /files/get_temporary_link REST endpoint
To get a temporary download link for every revision of a file in path
, use this GraphQL query:
demo_client/src/FolderRevisions.jsx:
query FolderRevisions($path: String) {
filesListFolder(path: $path) {
id
name
tag
path_display
revisions {
id
server_modified
temporaryDownloadLink
}
}
}
To rename a file and refresh that would change the query above, use this GraphQL mutation:
demo_client/src/Rename.jsx:
mutation RenameFile($fromPath: String!, $toPath: String!) {
filesMove(fromPath: $fromPath, toPath: $toPath) {
id
name
tag
path_display
revisions {
id
server_modified
temporaryDownloadLink
}
}
}
Warning: this is a proof of concept and is not meant for production. Anyone with access to your source code, the web client, or GraphQL server will have full read & write access to the /Apps/ApolloClientDemo folder in your Dropbox!
Clone this repo:
git clone https://github.com/mjlyons/apollo-client-demo.git && cd apollo-client-demo
Configure your Dropbox App:
-
Create the Dropbox app
- Go to https://www.dropbox.com/developers/apps/create
- Select "Dropbox API" (not "Dropbox Business API")
- Choose "App folder" access (this will limit access to /Apps/ApolloClientDemo in your Dropbox)
- If you have a Personal and Work Dropbox, pick one.
- Click the "Create app" button
-
Get a Dropbox access token
- In your app settings, find the "OAuth2 section"
- Look for the "Generated access token" settings
- Click the "Generate" button
- Store the resulting token by creating
common-src/.env.mjs
:
// Replace the Dropbox access token below export const DROPBOX_ACCESS_TOKEN = "x_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxx"; export default { DROPBOX_ACCESS_TOKEN };
-
Put some files in your Dropbox's /Apps/ApolloClientDemo folder
- Store a few files using https://www.dropbox.com or Dropbox's desktop client.
- Save changes to the files and overwrite the originals (to create multiple file revisions)
Set up the client:
cd demo-client
yarn install
yarn start
This will open the web client in your browser. You should see an entry for each file in the /Apps/ApolloClientDemo folder. Each file will have a timestamp for each revision, and clicking the timestamp will download that revision of the file.
Set up the server (optional): You only need to set the server up if you want to run some or all of your query outside of the client (options 2 & 3 at start of README).
First, create a service called apollo-client-demo
on Apollo Engine. It will instruct you to create a .env
file. Put a copy of that file in /demo-server
(for reporting stats) and in /demo-client
(for using the VSCode plugin).
Next, start up the server:
cd ../demo-server
yarn install
yarn start
This will run the GraphQL server on http://localhost:4000 and includes GraphQL Playground.
You should be able to visit http://localhost:4000 and run the following query:
query FolderRevisions {
filesListFolder {
id
name
tag
path_display
revisions {
id
server_modified
temporaryDownloadLink
}
}
}
This should return the same data rendered in the client install instructions.
By default, the web client will translate the GraphQL query locally and make Dropbox REST API requests. It does not use the GraphQL server. Stop running the server and try loading the page. It still loads.
Let's check what Dropbox REST API requests are sent by the client. In Chrome, open the "network" tab and refresh the page. You should see the Dropbox API requests list_folder
, list_revisions
, and get_temporary_link
. If you installed Apollo Dev Tools, you may see graphql
requests which are unrelated to the what's powering the web client UX.
In the "network" tab, find the Waterfall toward the right. You might need to make your browser window wider
to see it. You'll notice that the Dropbox REST API calls are parallelizing. As soon as the list_folder
call returns with the list of files, list_revisions
is called for each file simultaneously. This happens again when the list_revisions
calls return and get_tempoprary_link
is sent for each revision.
First, make sure you've installed Apollo Client Devtools in Chrome. Then switch to the "Apollo" tab and click the "Queries" icon. You should see the FolderRevisions GraphQL query powering the UX. Click the "Cache" icon next. You should see cached entries for the root query, each FileEntry (represents a file) and each FileRevision (represents a revision of a file).
Let's see what happens if we try to reload the same data from the GraphQL query by the folder contents twice.
In demo-client/src/App.js, find this block of code:
{/* Loading from root again to see if it triggers additional Dropbox API requests -- it shouldn't */}
{/*
<hr />
<FolderRevisionsWithData />
*/}
and uncomment it to look like this:
{/* Loading from root again to see if it triggers additional Dropbox API requests -- it shouldn't */}
<hr />
<FolderRevisionsWithData />
When you save and reload, you'll see the contents of the folder twice. However, there are no additional network requests in the "network" tab. Apollo Client is correctly caching the responses and preventing unnecessary network requests.
The cache is updated when mutations modify existing items in the cache and return updated data. For example, executing the filesMove
mutation automatically updates the FolderRevisions
query. There's no need to add new code, such as a response handler, to update the cache.
Translating the GraphQL query to REST API requests on the client has a downside: you need to make multiple round trips to load your data. Switching to the GraphQL server will shrink the client's network roundtrips down to one request.
To switch to the GraphQL server, start the server and remove the @client
directive from the GraphQL query.
In demo-client/src/FolderRevisions.jsx find the query:
const FILES_LIST_FOLDER_QUERY = gql`
query FolderRevisions($path: String) {
filesListFolder(path: $path) @client {
id
name
tag
path_display
revisions {
id
server_modified
temporaryDownloadLink
}
}
}
`;
and remove the @client
directive like so:
const FILES_LIST_FOLDER_QUERY = gql`
query FolderRevisions($path: String) {
filesListFolder(path: $path) {
id
name
tag
path_display
revisions {
id
server_modified
temporaryDownloadLink
}
}
}
`;
The @client
tells Apollo Client to resolve the GraphQL client locally. Removing it causes Apollo Client to send the query to the GraphQL server.
After saving and reloading, look at Chrome's "network" tab. You should notice a new request to http://localhost:4000
. You can see the GraphQL query in the request body and the full result in the response.
Let's say you wanted the filesListFolder
query to run on the GraphQL server, but the "revisions" and "temporaryDownloadLink" to be translated locally. You might want to do this if you were confident that the filesListFolder part of your schema was set but didn't want to lock in the other two yet.
You can do this by changing where you position the @client
directive in your query. Anything inside the @client
block will be run locally on the client; everything else will go to the GraphQL server.
For the case above, you would rewrite your query as:
const FILES_LIST_FOLDER_QUERY = gql`
query FolderRevisions($path: String) {
filesListFolder(path: $path) {
id
name
tag
path_display
revisions @client {
id
server_modified
temporaryDownloadLink
}
}
}
`;
If you save this change, reload, and watch the "network" tab, you'll notice the graphql query to http://localhost:4000 only includes the following:
query FolderRevisions($path: String) {
filesListFolder(path: $path) {
id
name
tag
path_display
__typename
}
}
You'll also see the client is making list_revisions
and get_temporary_link
REST API calls again.
You could extend the "gradual rolllout" approach to ramp up traffic to a specific resolver rather than switching from 0% to 100%. Using feature gating, you'd start by removing the @client directive for a small percentage of clients. As you feel more comfortable sending additional traffic to the server's resolver you would adjust the feature gate to remove the @client directive for more clients. Eventually you'll be out to 100% and fully rolled out.
The code to decompose GraphQL into REST API calls is shared between client & server in common-src/resolvers.mjs
. You don't need to do write any new code to move parts of your GraphQL schema from the client to a GraphQL server.