See all posts

Handling GraphQL-Codegen's need for introspection in production

A way to use GraphQL-Codegen without having to commit the generated schema files to your repository or creating a security risk.

GraphQL-Codegen is an awesome tool that simply introspects your GraphQL Backend and generates the schema types for your frontend. It requires little to no configuration and is a great way to catch bugs early on in your schema. I replaced all the manually written types in my existing react apps and the code reduction was a humongous 30%!

But, there is a small problem. It needs introspection to be turned on for it to work. Easy enough in development, but a security risk in production. You don’t want to expose your schema to the world. So, what do you do?

Option 1: Commit the schema files

Simplest solution. Just commit the schema files to your repository. But, it has a few drawbacks. Mainly because:

  1. It bloats your repository
  2. The schema files might be for a different version of the schema (a development one, a local one or one that is still under review)

Option 2: Temporarily turn on introspection when the frontend is being built

We tried this approach and it’s a bad one. Just before you build your frontend, you turn on introspection, and after you’re done, turn it off. This requires a lot of server restarts, is a pain to keep track of and is just not a good idea.

Option 3: Use a different introspection endpoint (Worked)

This is the approach that we finally settled on.

On the backend:

  1. Create a new graphql endpoint that is known only to us. Ex: https://api.example.com/graphql-2f530e6e-bd6f-4c7b-87b6-d848e1407c7d
  2. Turn on introspection solely for this endpoint
  3. Remove all permissions

On the frontend:

  1. Store the endpoint as a build-time environment variable, so that it is not exposed to the outside world in the build file.
  2. Use the variable in the codegen configuration
  3. Run the codegen script as per usual

Implementation

// BACKEND
// server.ts
const server = new ApolloServer({
  schema: applyMiddleware(schema, permissions),
  introspection: false,
  ...rest,
});

const serverInternal = new ApolloServer({
  schema: applyMiddleware(schema, servicePermissionsWithAllDisabled),
  introspection: true,
  ...rest,
});

app.use("/graphql", cors<cors.CorsRequest>(), json(), ...rest);

// Internal endpoint for codegen
app.use(
  "/graphql-2f530e6e-bd6f-4c7b-87b6-d848e1407c7d",
  cors<cors.CorsRequest>(),
  json(),
  ...rest,
);
// FRONTEND
// codegen.ts
import { CodegenConfig } from "@graphql-codegen/cli";
import dotenv from "dotenv";

dotenv.config();

const config: CodegenConfig = {
  schema: process.env.INTERNAL_GRAPHQL_BACKEND,
  // this assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
  documents: ["src/**/*.{ts,tsx}"],
  generates: {
    "./src/__generated__/": {
      preset: "client",
      plugins: [],
      presetConfig: {
        gqlTagName: "gql",
      },
    },
  },
  ignoreNoDocuments: true,
};

export default config;

Agreed, it takes a little bit more of the server resources to run another endpoint, but it’s a small price to pay for the convenience (and security) that you get.

It still feels like a hack, so if you have a better solution, please let me know.

Written by Arun kumar Gandlur on