Create a graphql proxy server using wrapSchema

In this article we are going to create a GraphQL proxy server using wrapSchema module from graphql-tools and filter out one mutation from our proxy server. A need for such case would be when we want to allow only a limited set of graphql queries/mutations to be disclosed to the UI hiding others behind a proxy. The application would:

  • fetch GraphQL schema from main GraphQL server
  • filter out a login mutation from this fetched schema
  • delegate the calls that we receive to our proxy server to main GraphQL server
  • create a GraphQL proxy server using wrapSchema module from graphql-tools

We have two server here:

  • Proxy server – which we will be creating in this application
  • Main GraphQL server – this is the server that we want to proxy behind our proxy server

For this proxy server we will need to install ApolloServer, graphql, graphql-tools, @graphql-tools using below command:

 npm i apollo-server graphql graphql-tools @graphql-tools/wrap 

graphql: We will be using the main graphql module

apollo-server: We will be using apollo-server to create a GraphQL server

graphql-tools: We will be using @graphql-tools/wrap and @graphql-tools/delegate tools from this tool kit


Let’s start

Obviously we will start with the package.json file

{
  "name": "graphql-proxy",
  "version": "1.0.0",
  "description": "A graphql proxy server",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 0"
  },
  "author": "Nikhil",
  "license": "MIT",
  "dependencies": {
    "@graphql-tools/wrap": "^7.0.5",
    "apollo-server": "^2.21.0",
    "cross-fetch": "^3.0.6",
    "graphql": "^15.5.0",
    "graphql-tools": "^4.0.8"
  }
}

We will start here by getting the schema from our main GraphQL server

const serverSchema = await introspectSchema(executor)

Now let’s examine what we are using and how it can help us achieve our final goal.

introspectSchema(executor)

The introspectSchema call here will fetch the complete schema from our main GraphQL server and return it. This introspectSchema function is passed a function as parameter called executor. This executor function will be responsible for making the actual rest calls to the main GraphQL server and return the data. Please see the executor function below:

const executor = async ({ document, variables, context }) => {
    const query = print(document);
    const fetchResult = await fetch(<main-graphql-server-url>, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ query, variables })
    });
    return fetchResult.json();
}

In the executor function we receive document, variable and context as parameters. We are using the print function from graphql module which will parse the document AST and return us the query content in standard format. So in simple terms the document here is an object and the print function will return us the GraphQL query; variable is the variables object that we pass to GraphQL query/mutation and context is the object that we have returned from context field function when we create ApolloServer instance.

const schema = wrapSchema({
    schema: serverSchema,
    executor,
    transforms: [
        new FilterRootFields(loginFilter)
    ],
    createProxyingResolver: applicationProxyResolver
});

wrapSchema

Now next step is to wrap this schema using wrapSchema function. This will wrap the main GraphQL server schema and create a wrapper over it using the fields that we pass to this function.

schema: We just pass the complete schema that we have received from out main GraphQL server to this field.

executor: We pass the executor function that we have seen above. Here all out calls will use the same executor function to make calls to the main GraphQL server.

transforms: Here we have passed a new FilterRootFields instance which takes a function as parameter which us loginFilter in our case. The loginFilter function returns boolean values and depending on the value the operation will be allowed or filtered from our proxy server.

const loginFilter = (operationName, fieldName) => {
  return !(operationName == "Mutation" && fieldName == "login")
}

Our main GraphQL server has a login mutation and we want to filter this from our proxy server. In the loginFilter function we are checking that if the operationName is “Mutation” and fieldName is “login” then we return false. Due to this code we will not have any login mutation in our proxy server. So simple right 🙂

createProxyingResolver: This is function from where we need to delegate the proxy call to the main GraphQL server. The basic implementation is below where you may see that we will be delegating the request which will internally call the exceutor to get data from our main GraphQL server.

import * as delegate from "@graphql-tools/delegate";

export const applicationProxyResolver = ({
    subschemaConfig,
    operation,
    transformedSchema,
    fieldName,
  }) => {
    return (_parent, _args, context, info) => {
        return delegate.delegateToSchema({
            schema: subschemaConfig,
            operation,
            context,
            info,
            transformedSchema,
        });
    };
};

ApolloServer

Now we have our wrap schema or proxy schema ready and we will pass this to the ApolloServer where we will also be adding a context to each request that we receive for our proxy.

const server = new ApolloServer({ 
    schema,
    context: (args) => {
        const { req, res } = args
        const authorizationKey = req?.headers?.authorization
        // fetch user information using authorizationKey
        // const user = fetch(...)
        // pass the information in context
        res.header('content-type', 'application/json')

        return {
            authorization: authorizationKey,
            // Pass user information in context 
            // so that it is available to each request
            // user: user,
        };
        }
});

The last thing is to start our server using server.listen function. We will pass the port on which we want our application proxy to run as parameter to listen function.

// The `listen` method launches a web server.
server.listen(<PROXY_APP_PORT>).then(({ url }) => {   
    console.log(`🚀  Server ready at ${url}`);
});

Our final code block

const run = async () => {
    const serverSchema = await introspectSchema(executor)
    const schema = wrapSchema({
      schema: serverSchema,
      executor,
      transforms: [
        new FilterRootFields(loginFilter)
      ],
      createProxyingResolver: applicationProxyResolver
    });

    const server = new ApolloServer({ 
        schema,
        context: (args) => {
            const { req, res } = args
            const authorizationKey = req?.headers?.authorization
            // fetch user information using authorizationKey
            // const user = fetch(...)
            // pass the information in context
            res.header('content-type', 'application/json')

            return {
                authorization: authorizationKey,
                // Pass user information in context 
                // so that it is available to each request
                // user: user,
            };
          }
    });

    // The `listen` method launches a web server.
    server.listen(<PROXY_APP_PORT>).then(({ url }) => {
      console.log(`🚀  Server ready at ${url}`);
    });
}

run()

Conclusion

Here we have seen how easily we can create a proxy server and filter out queries/mutations by using wrapSchema method from graphql-tools. We need to handle subscription here but will handle that in another blog. Thanks for reading 🙂

Tags: