Skip to content Skip to sidebar Skip to footer

How To Split A Long GraphQL Schema

I am trying to create a Schema, however is going to get too long and confusing, what are the best practices to split the different queries, mutations and inputs so I can just requ

Solution 1:

There are multiple options, here are three of them:

  1. You can take a look at Apollo's blog - they show a way to modularize the schema: Modularizing your GraphQL schema code If you want an example, you can take a look at this github repository

  2. Of course you can always just use the template literals to embed parts of the schema as strings:

const countryType = `
type Country {
  _id: ID!
  name: String!
  region: [Region!]!
}
`

const regionType = `
type Region {
  _id: ID!
  name: String!
  countries: [Country!]
}
`

const schema = `
${countryType}
${regionType}

# ... more stuff ...
`

module.exports = buildSchema(schema);
  1. Another way is to use the code first approach and write the schemas in javascript instead of graphql schema language.

Solution 2:

Make it separate folder and structure as well to make codes maintainable I do the following:

GraphQL Example Repository

File Structure Screenshot

const express = require('express');
const glob = require("glob");
const {graphqlHTTP} = require('express-graphql');
const {makeExecutableSchema, mergeResolvers, mergeTypeDefs} = require('graphql-tools');
const app = express();
//iterate through resolvers file in the folder "graphql/folder/folder/whatever*-resolver.js"
let resolvers = glob.sync('graphql/*/*/*-resolver.js')
let registerResolvers = [];
for (const resolver of resolvers){
// add resolvers to array
    registerResolvers = [...registerResolvers, require('./'+resolver),]
}
//iterate through resolvers file in the folder "graphql/folder/folder/whatever*-type.js"
let types = glob.sync('graphql/*/*/*-type.js')
let registerTypes = [];
for (const type of types){
// add types to array
    registerTypes = [...registerTypes, require('./'+type),]
}
//make schema from typeDefs and Resolvers with "graphql-tool package (makeExecutableSchema)"
const schema = makeExecutableSchema({
    typeDefs: mergeTypeDefs(registerTypes),//merge array types
    resolvers: mergeResolvers(registerResolvers,)//merge resolver type
})
// mongodb connection if you prefer mongodb
require('./helpers/connection');
// end mongodb connection
//Make it work with express "express and express-graphql packages"
app.use('/graphql', graphqlHTTP({
    schema: schema,
    graphiql: true,//test your query or mutation on browser (Development Only)
}));
app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');

Solution 3:

You could use Type definitions (SDL) merging of graphql-tools as well.

This tools merged GraphQL type definitions and schema. It aims to merge all possible types, interfaces, enums and unions, without conflicts.

The doc introduces multiple ways to merge SDL.

I made an example of modularizing-the-schema using apollo-server tech stack.


Solution 4:

In typescript, you can put your graphQL schemas in file that you read from:

import { readFileSync } from "fs";
import path from "path";

function getTypeDefs(filename: string) {
    return gql(readFileSync(path.join(process.cwd(), 'resources', filename), 'utf8'));
}

Having a file structure looking like:

.
├── src
│   └── server.ts
└── resources
    ├── other.graphql
    └── schema.graphql

You can pass multiple gql in an array like:

import { ApolloServer, gql } from "apollo-server-express";
import { makeExecutableSchema } from "graphql-tools";

const server = new ApolloServer({
    schema: makeExecutableSchema({
        typeDefs: [getTypeDefs('schema.graphql'), getTypeDefs('other.graphql')],
        resolvers: YourResolvers
    })
});

Solution 5:

the best way follow my code s

i m use npm typescripter express starter

export interface IGraphqlSchema {
  type?: string,
  query?: string,
  input?: string,
  mutation?: string,


}


export const sharedGraphqlSchema = {
  scalar: `
      scalar Void
      scalar Any
 `,

  rootSchema:
    `
schema {
query: RootQuery
mutation: RootMutation
}`
  ,
  rootQuery: "\ntype RootQuery {",

  rootMutation: "\ntype RootMutation {",


};


import { IGraphqlSchema } from "core/interfaces/graphql.schema";
import { sharedGraphqlSchema } from "./shared.graphql.schema";


export function combineSchemaGraphql(schemas: IGraphqlSchema[]): string {

  let combine: string = "";
  combine = combine.concat(sharedGraphqlSchema.scalar);
  let temp: string = "";


  for (let index = 0; index < 4; index++) {
    for (let j = 0; j < schemas.length; j++) {
      const item = schemas[j];
      switch (index) {
        case 0:
          combine = combine.concat(item.type);
          break;
        case 1:
          combine = combine.concat(item.input);
          break;
        case 2:
          temp = temp.concat(item.query);

          break;

        default:
          temp = temp.concat(item.mutation);
          break;
      }

    }

    if (index == 2) {
      combine = combine.concat(sharedGraphqlSchema.rootQuery);
      combine = combine.concat(temp);
      combine = combine.concat("\n}");
      temp = "";
    }
    if (index == 3) {
      combine = combine.concat(sharedGraphqlSchema.rootMutation);
      combine = combine.concat(temp);
      combine = combine.concat("\n}");
    }

  }

  combine = combine.concat(sharedGraphqlSchema.rootSchema);

  return combine;
}


import { IGraphqlSchema } from "../../../core/interfaces/graphql.schema";

export const profileSchema: IGraphqlSchema = {

  type:
    `
  type Profile {
      _id: ID!
     firstName: String
     lastName: String
     userName: String
     password: String
     email: String
     phone: String
     image: String
     address: String
     gender: Boolean
     country: String
     city: String
     active: Boolean
     status: Boolean
     statusMessage: String
     createdAt: String
     updatedAt: String
     deletedAt: String
    }
   `,
  input:
    `
     input ProfileInputData {
         image: String
        firstName: String
        lastName: String
        country: String
        city: String
        password: String
        passConfirm: String
   }`,
  query:
    `
     profile: Profile!
`,
  mutation: `
 profileUpdate(inputs: ProfileInputData): Void
 `
};


import { NextFunction, Request, Response } from "express";
import { StatusCodes } from "http-status-codes";
import { default as i18n } from "i18next";
import { RequestWithUser } from "../../auth/interfaces/reqeust.with.user.interface";
import ProfileService from "../services/profile.service";
import { UserEntity } from "../entities/user.entity";
import { IUser } from "../../auth/interfaces/user.interface";
import { isEmpty } from "./../../shared/utils/is.empty";
import { IMulterFile } from "./../../shared/interfaces/multer.file.interface";
import { optimizeImage } from "./../../shared/utils/optimize.image";
import { commonConfig } from "./../../common/configs/common.config";
import { IUserLogIn } from "@/modules/auth/interfaces/Log.in.interface";
import { ProfileValidation } from "@/modules/common/validations/profile.validation";
import { HttpException } from "@/core/exceptions/HttpException";
import { validateOrReject, Validator } from "class-validator";
import { warpValidationError } from "@/core/utils/validator.checker";
import { sharedConfig } from "@/modules/shared/configs/shared.config";

export const ProfileResolver = {

  profile: async function({ inputs }, req: RequestWithUser): Promise<void | any> {


    const user: IUserLogIn = req.user;

    const profileService = new ProfileService();
    const findOneData: IUser = await profileService.show(user._id);

    return {
      ...findOneData._doc
    };

  },


  profileUpdate: async function({ inputs }, req: RequestWithUser): Promise<void | Object> {

    const profileValidation = new ProfileValidation(inputs);

    try {
      await validateOrReject(profileValidation);

    } catch (e) {
      warpValidationError(e);
    }

    const user: IUserLogIn = req.user;
    const userEntity = new UserEntity(inputs);
    await userEntity.updateNow().generatePasswordHash();
    const profileService = new ProfileService();
    if (!isEmpty(req.file)) {

      const file: IMulterFile = req.file;
      // userEntity.image = commonConfig.profileDirectory + file.filename;
      userEntity.image = sharedConfig.publicRoot + file.filename;
      await optimizeImage(file.destination + file.filename, 200, 200, 60);
    }

    const updateData: IUser = await profileService.update(user._id, userEntity);



  }


};



import { settingSchema } from "@/modules/common/schemas/setting.schema";

process.env["NODE_CONFIG_DIR"] = __dirname + "/core/configs";
import "dotenv/config";
import App from "./app";
import { merge } from "lodash";
import { combineSchemaGraphql } from "./core/utils/merge.graphql.type";
import { authSchema } from "@/modules/auth/schemas/auth.schema";
import { profileSchema } from "@/modules/common/schemas/profile.schema";
import { AuthResolver } from "@/modules/auth/resolvers/auth.resolver";
import { ProfileResolver } from "@/modules/common/resolvers/profile.resolver";
import { SettingResolver } from "@/modules/common/resolvers/setting.resolver";
import { sharedSchema } from "@/modules/shared/schemas/shared.schema";

const rootQuery = combineSchemaGraphql(
  [
    sharedSchema,
    authSchema,
    profileSchema,
    settingSchema]);

const mutation = merge(
  AuthResolver,
  ProfileResolver,
  SettingResolver);

const app = new App(rootQuery, mutation);
app.listen();



import { AppInterface } from "./core/interfaces/app.interface";

process.env["NODE_CONFIG_DIR"] = __dirname + "/core/configs";

import compression from "compression";
import cookieParser from "cookie-parser";
import config from "config";
import express from "express";
import helmet from "helmet";
import hpp from "hpp";
import morgan from "morgan";
import { connect, set } from "mongoose";
import swaggerJSDoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
import * as path from "path";
import * as fs from "fs";
import i18nMiddleware from "i18next-express-middleware";
import { default as i18n } from "i18next";
import Backend from "i18next-node-fs-backend";
import { LanguageDetector } from "i18next-express-middleware";
import { dbConnection } from "./core/databases/database.config";
import errorMiddleware from "./core/middlewares/error.middleware";
import { logger, stream } from "./core/utils/logger";
import contentNegotiationMiddleware from "./modules/common/middlewares/content.negotiation.middleware";
import corsMiddleware from "./modules/common/middlewares/cors.middleware";
import userAgent from "express-useragent";
import { graphqlHTTP } from "express-graphql";
import { buildSchema } from "graphql";
import authMiddleware from "@/modules/auth/middlewares/auth.middleware";
import multer from "multer";
import { multerFunctions, multerFileFilter } from "@/modules/shared/utils/multer.functions";
import { sharedConfig } from "@/modules/shared/configs/shared.config";

class App implements AppInterface {
  public app: express.Application;
  public port: string | number;
  public env: string;

  constructor(schema: string, resolver: object) {
    this.app = express();
    this.port = process.env.PORT || 4000;
    this.env = process.env.NODE_ENV || "development";
    this.initializeI18n();
    this.connectToDatabase();
    this.initializeMiddlewares();
    this.initializeSwagger();
    this.initializeErrorHandling();
    this.initGraphql(schema, resolver);

  }

  public listen(): void {
    this.app.listen(this.port, () => {
      logger.info(`====  typescript express.js modular graphql  kick starter ====`);
      logger.info(`===== by yasin palizban ===== `);
      logger.info(`https://github.com/yasinpalizban`);
      logger.info(`======= ENV: ${this.env} =======`);
      logger.info(`🚀 App listening on the port ${this.port}`);

    });
  }

  public getServer(): express.Application {
    return this.app;
  }

  private connectToDatabase(): void {
    if (this.env !== "production") {
      set("debug", true);
    }

    connect(dbConnection.url, dbConnection.options);

  }

  private initializeMiddlewares(): void {
    this.app.use(morgan(config.get("log.format"), { stream }));
    this.app.use(corsMiddleware);
    this.app.use(hpp());
    this.app.use(helmet());
    this.app.use(compression());
    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: true }));
    this.app.use(cookieParser());
    this.app.use(userAgent.express());

    this.app.use("/public", express.static(path.join(__dirname, "public")));
    this.app.use(authMiddleware);

    this.app.use(contentNegotiationMiddleware);


    const storage = multer.diskStorage({
      destination: sharedConfig.publicRoot,
      filename: multerFunctions
    });
    const maxSize = 4 * 1000 * 1000;
    const upload = multer({ storage: storage, fileFilter: multerFileFilter, limits: { fileSize: maxSize } });
    this.app.use(upload.array("image"));

  }


  private initializeSwagger(): void {
    const options = {
      swaggerDefinition: {
        info: {
          title: "REST API",
          version: "1.0.0",
          description: "Example docs"
        }
      },
      apis: ["swagger.yaml"]
    };

    const specs = swaggerJSDoc(options);
    this.app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));
  }


  private initializeI18n(): void {

    i18n
      .use(Backend)
      .use(LanguageDetector)
      .init({
        lng: "en",
        whitelist: ["en", "fa"],
        fallbackLng: "en",
        // have a common namespace used around the full app
        ns: ["translation"],
        debug: false,
        backend: {
          loadPath: path.join(__dirname + "/locales/{{lng}}/{{ns}}.json")
          // jsonIndent: 2
        },
        preload: ["en", "fa"]
      });
    this.app.use(i18nMiddleware.handle(i18n));

  }

  private initializeErrorHandling(): void {
    this.app.use(errorMiddleware);
  }

  private initGraphql(schema: string, resolver: object): void {

    this.app.use(
      "/graphql",
      graphqlHTTP({
        schema: buildSchema(schema),
        rootValue: resolver,
        graphiql: true,
        customFormatErrorFn: (error) => ({
          message: error.message,
          locations: error.locations,
          stack: error.stack ? error.stack.split('\n') : [],
          path: error.path,
        })
      //  customFormatErrorFn(err) {
          // if (!err.originalError) {
          //   return err;
          // }
          //
          // // @ts-ignore
          // const data = err.originalError.data;
          // const message = err.message || "An error occurred.";
          // // @ts-ignore
          // const code = err.originalError.status || 500;
          //
          // return { message: message, status: code, data: data };


      //  }
      })
    );
  }

}

export default App;


your well come


Post a Comment for "How To Split A Long GraphQL Schema"