Issues with hot reloading in Express.js - GraphQL API are preventing it from functioning properly

In an attempt to create a mockAPI with hot reload functionality, I encountered an issue. Whenever I edit the schema or the data returned by the server and restart it, the changes are not reflected (I tested this using Postman).

I believed that restarting the server after making edits to the schema or data would solve the issue. However, even utilizing a callback function within graphqlHTTP to listen for changes in the schema and rootValue did not work.

Please note: I do not want to terminate the Node process!

index.js

/** Require file system */
const fs = require('fs');
const md5 = require('md5');
const path = require('path');
const { readFileSync } = require('fs');
/** Start MockAPI */
const http = require('http');
const expressModule = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

module.exports = class mockAPI {
  constructor(options = { host: '127.0.0.1', port: 3000, namespace: '' }) {
    /** Initialize express */
    this.modules();

    this.directories = {
      data: './data',
      schema: './schema'
    };

    this.ignore = ['.DS_Store', 'Thumbs.db'];

    this.host = options.host;
    this.port = options.port;
    this.namespace = options.namespace;

    this.registerRoutes({
      data: this.getRoutes(this.directories.data),
      schema: this.getRoutes(this.directories.schema)
    });

    /** Begin watching for changes (true) */
    this.start(true);
  }

  modules() {
    /** Initialize express */
    this.express = expressModule();
    this.server = http.createServer(this.express);
  }

  start(watch) {
    /** Start server */
    return new Promise((resolve, reject) => {
      this.server
        .listen(this.port, this.host, () => {
          const restart = !watch ? 're' : '';

          console.info(
            '\x1b[1m\x1b[32m%s\x1b[0m\x1b[0m',
            `\n> MockAPI server ${restart}open on ... http://${this.server.address().address}:${this.server.address().port}\n`
          );
          /** Begin watching API files */
          if (watch) this.watch(this.directories);
        })
        .on('error', err => {
          if (err) reject(err);
        });

      resolve();
    });
  }

  stop(restart) {
    /** Stop server */
    return new Promise((resolve, reject) => {
      this.server.close(err => {
        if (err) reject(err);

        const color = restart ? '33' : '31';
        const message = restart ? 'restart' : 'closed';

        console.info(
          `\x1b[1m\x1b[${color}m%s\x1b[0m\x1b[0m`,
          `> MockAPI server ${message} on ... http://${this.host}:${this.port}`
        );

        resolve();
      });
    });
  }

  watch(directories) {
    this.md5Previous = null;
    this.fsWait = false;

    Object.keys(directories).map(key => {
      const dir = directories[key];

      fs.watch(path.resolve(__dirname, dir), { recursive: true }, (event, filename) => {
        if (event === 'change' && filename) {
          /** Avoid executing multiple times while saving */
          if (this.fsWait) return false;

          const md5Current = md5(fs.readFileSync(path.resolve(__dirname, `${dir}/${filename}`));
          /** Compare file hashes */
          if (md5Current === this.md5Previous) return false;

          /** Restart server */
          this.fsWait = true;
          setTimeout(async () => {
            this.fsWait = false;
            this.md5Previous = md5Current;

            console.info('\x1b[33m%s\x1b[0m', `- ${filename} changed`);
            await this.server.removeAllListeners('upgrade');
            await this.server.removeAllListeners('request');

            await this.stop(true);

            await this.modules();
            await this.registerRoutes({
              data: this.getRoutes(this.directories.data),
              schema: this.getRoutes(this.directories.schema)
            });

            this.start(false);
          }, 1000);
        }
      });
    });

    return true;
  }

  getRoutes(dir) {
    const absPath = path.resolve(__dirname, dir);
    let routes = [];

    const readDir = path => {
      let data = [];

      fs.readdirSync(path, { withFileTypes: true }).forEach(file => {
        if (this.ignore.includes(file.name)) return false;

        if (file.isDirectory()) return readDir(`${path}/${file.name}`);

        const route = path === absPath ? '' : path.replace(absPath, '');
        const name = file.name.replace(/\.js|\.graphql/, '');
        const endpoint = name.replace(/\s+/g, '-');

        data.push({
          name: name,
          dir: route,
          file: file.name,
          route: `${route}/${endpoint}`
        });
      });

      routes = [...routes, ...data];
    };
    readDir(absPath);
    /** Sort array by route */
    return routes.sort((a, b) => a.route.localeCompare(b.route));
  }

  registerRoutes(routes) {
    if (routes.schema.length !== routes.data.length) {
      console.error(
        '\x1b[31m%s\x1b[0m',
        `- schema.length(${routes.schema.length}) !== data.length(${routes.data.length}).\n  Each schema must match a data file in the same file structure with the same file name.`
      );
      process.exit;
    }

    routes.data.forEach(async (vdata, key) => {
      if (vdata.route === routes.schema[key].route) {
        const typeDefs = await readFileSync(`./mock-api/schema${routes.schema[key].dir}/${routes.schema[key].file}`).toString('utf-8');

        let schema = await buildSchema(typeDefs);
        let data = await require(`./data${vdata.dir}/${vdata.file}`);

        this.express.use(
          this.namespace + vdata.route,
          graphqlHTTP({
            schema: schema,
            rootValue: data,
            graphiql: true
          })
        );
      }
    });
  }
};

I would appreciate any assistance in resolving this issue.

To replicate the process, simply create directories /data and /schema in the same root directory as this code/snippet/file and run any yarn or npm command to load this module (index.js). Place a JS file in the /data directory to define the resolvers and a GraphQL file in the /schema directory to define the schema. Both files must have the same name to match the route registered based on the file structure.

For example, if you place /data/test/data.js in /data, write the schema in /schema/test/data.graphql, and access it via .

Below are examples for quick replication:

data.js

/**
 * Returns GraphQL data
 * @returns {object} data
 */
module.exports = {
  hello: () => {
    return 'Hello world!';
  }
};

data.graphql

type Query {
  hello: String
}

What I require is a method to flush, clear, delete, or otherwise remove the stored data.

Answer №1

To resolve the issue, a child process was created using spawn.

index.js

/** file system */
const { spawn } = require('child_process');
/** require environment variables */
let env = require('../config/env');
/**
 * Spawn Mock API server as child process
 * @param {boolean} initial process
 * @returns {object} process data
 * --------------------------------
 */
const server = initial => {
  /** merge environment variables */
  env = Object.assign(process.env, env, { INITIAL: initial });

  /** start child process for the Mock API */
  let child = spawn('node', ['mock-api/MockAPI.js'], {
    env: env
  });

  /** child info/logs */
  child.stdout.on('data', (data) => {
    try {
      /** check if data is object */
      const obj = JSON.parse(data.toString());
      /** handle object data */
      if ({}.hasOwnProperty.call(obj, 'restart')) {
        child.kill();
      }
    } catch (error) {
      console.log(data.toString());
    }
  });

  /** child errors */
  child.stderr.on('data', (data) => {
    console.error(
      '\x1b[1m\x1b[31m%s\x1b[0m\x1b[0m',
      `> Mock API server crashed on ... http://${env.MOCK_API_HOST}:${env.MOCK_API_PORT}\n`,
      data.toString()
    );
  });

  child.on('close', () => {
    /** Wait for process to exit, then run again */
    setTimeout(() => server(false), 500);
  });

  /** kill child process if parent process gets killed */
  process.on('exit', () => child.kill('SIGINT'));

  return child;
};

module.exports = (() => server(true))();

MockAPI.js

/** file system */
const fs = require('fs');
const md5 = require('md5');
const path = require('path');
const { readFileSync } = require('fs');
/** server data */
const http = require('http');
const expressModule = require('express');
const express = expressModule();
const server = http.createServer(express);
/** graphql */
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
/** define globals */
const data = './data';
const ignore = ['.DS_Store', 'Thumbs.db'];
const env = process.env;
const host = env.MOCK_API_HOST || '127.0.0.1';
const port = env.MOCK_API_PORT || 3000;
const namespace = env.MOCK_API_NAMESPACE || '';
/**
 * Retrieve routes data from ./data directory
 *
 * NOTE:
 * each graphql schema must
 * match a JS data file in
 * the same directory
 *
 * @param {string} directory
 * @returns {Array} routes
 */
const getRoutes = dir => {
  const root = path.resolve(__dirname, dir);
  const routes = [];

  const getData = path =>
    fs.readdirSync(path, { withFileTypes: true }).forEach(file => {
      if (ignore.includes(file.name) || file.name.endsWith('.graphql')) return false;

      if (file.isDirectory()) return getData(`${path}/${file.name}`);

      const name = file.name.replace(/\.js/, '');
      const route = path === root ? '' : path.replace(root, '');
      const endpoint = name.replace(/\s+/g, '-');

      routes.push({
        name: name,
        data: file.name,
        directory: route,
        graphql: `${name}.graphql`,
        route: `${route}/${endpoint}`
      });
    });
  getData(root);

  return routes;
};
/**
 * Register all routes
 *
 * NOTE:
 * each graphql schema must
 * match a JS data file in
 * the same directory
 *
 * @param {Array} routes
 */
const registerRoutes = routes => {
  routes.forEach(async route => {
    const graphqlPath = path.resolve(__dirname, `./data${route.directory}/${route.graphql}`);
    await fs.stat(graphqlPath, (error, stats) => {
      error, stats;
    });

    const dataPath = path.resolve(__dirname, `./data${route.directory}/${route.data}`);
    await fs.stat(dataPath, (error, stats) => {
      error, stats;
    });

    const typeDefs = await buildSchema(readFileSync(graphqlPath).toString('utf-8'));
    const rootValue = await require(dataPath);

    express.use(
      namespace + route.route,
      graphqlHTTP({
        schema: typeDefs,
        rootValue: rootValue,
        graphiql: true
      })
    );
  });
};
/**
 * Start monitoring data files
 *
 * Stop current process if files
 * were edited and restart the server
 *
 * @param {String} directory
 */
const watchFiles = directory => {
  let md5Previous = null;
  let fsWait = false;

  fs.watch(path.resolve(__dirname, directory), { recursive: true }, (event, filename) => {
    if ((event === 'change' || event === 'rename') && filename) {
      /** Prevent multiple executions */
      if (fsWait) return false;

      const md5Current = md5(fs.readFileSync(path.resolve(__dirname, `${directory}/${filename}`)));
      /** Compare file hashes */
      if (md5Current === md5Previous) return false;

      /** Restart server */
      fsWait = true;
      setTimeout(async () => {
        fsWait = false;
        md5Previous = md5Current;

        console.info(
          '\x1b[33m%s\x1b[0m',
          `- ${filename} changed\n> Mock API server restarts on ... http://${host}:${port}`
        );

        /** Restart child process */
        console.log(JSON.stringify({ restart: true }));
      }, 2000);
    }
  });
};
/**
 * Initialize server
 */
const initializeServer = () => {
  registerRoutes(getRoutes(data));

  server
    .listen(port, host, () => {
      const status = env.INITIAL === 'true' ? 'open' : 'reopened';

      console.info(
        '\x1b[1m\x1b[32m%s\x1b[0m\x1b[0m',
        `> Mock API server ${status} on ... http://${server.address().address}:${
          server.address().port
        }`
      );

      /** Start monitoring data files */
      watchFiles(data);
    })
    .on('error', error => {
      if (error) console.log('[ERROR] Mock API: ', error);
    });
};

initializeServer();

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Route to POST cannot be found

Attempting to post a comment triggers an error that redirects me to the /campgrounds page when I submit it, as indicated in the code below. I'm puzzled about what I'm overlooking; here is the error message: Cast to ObjectId failed for value "5e6 ...

Encountering an issue with multi ./src/styles.scss during the deployment of my Angular application on Heroku

My attempt to deploy my Angular app on Heroku is resulting in an unusual error. I've experimented with changing the styles path in angular.json, but it hasn't resolved the issue. I suspect it's a path problem, yet I can't seem to corre ...

What happens when an OPTIONS request is processed with the `preFlightContinue` setting enabled?

Recently, I came across the concept of a "preflight OPTIONS request" in Express that I realize I don't fully understand. This is typically how it's handled: const cors = require('cors'); const app = express(); app.use(cors({ preflig ...

Updating fields in a MongoDB collection with identical names

I am seeking guidance on how to update all instances of 'list_price' within a complex collection like this: "cost_price" : 79.9, "list_price" : 189.9, "sale_price" : 189.9, "integrations" : { "erp" : { "mbm" : { "cost_pri ...

Modifying the attribute of an element inside an array

Presented below is an object: { "_id" : ObjectId("5a8d83d5d5048f1c9ae877a8"), "websites" : [ "", "", "" ], "keys" : [ { "_id" : ObjectId("5a8d83d5d5048f1c9ae877af"), "name ...

Steps for creating an NPM package setup

I am in the process of developing my first node.js package aimed at simplifying the usage of a REST API. However, I am encountering difficulties in configuring the package to allow users to seamlessly implement the following functionality within their appl ...

While the NPM package functions properly when used locally, it does not seem to work when installed from the

I have recently developed a command-line node application and uploaded it to npm. When I execute the package locally using: npm run build which essentially runs rm -rf lib && tsc -b and then npm link npx my-package arguments everything works sm ...

What is the best way to verify and eliminate unnecessary attributes from a JSON request payload in a node.js application?

My goal is to verify the data in the request payload and eliminate any unknown attributes. Example of a request payload: { "firstname":"john", "lastname":"clinton", "age": 32 } Required attributes: firstname and lastname Optional a ...

eliminating the __typename field from the urql query response prior to performing a mutation

When retrieving data from a GraphQL Server using urql, an additional _typename field is automatically included by urql to track the cache: { __typename "Book" name "test" description "the book" id "hPl3 ...

The process of parsing HashMap failed due to an unexpected encounter with an Array, when an Object

Currently, I am in the experimental phase of creating an action at Hasura using a Node.js code snippet hosted on glitch.com. The code snippet is as follows: const execute = async (gql_query, variables) => { const fetchResponse = await fetch( "http ...

Encountering a ForbiddenError due to an invalid CSRF token when using multer in a locally integrated image upload router

As part of ensuring the security of my Express application, I have implemented csurf to protect against cross-site forgeries. I have globally registered it, as shown in the code below, and everything has been working smoothly so far. Recently, I decided t ...

Ways to refresh the cache after performing delete, update, or add operations on data

I currently utilize a data caching tool called cache-all. However, I have encountered an issue where newly added information does not appear when displaying all data after the addition. To ensure that new data is immediately reflected in requests, I typica ...

Search for records in MongoDB where the time is below a specified threshold

content of the document is provided below chatid:121212, messages:[ { msg:'Hello', time:'2021-04-17T16:35:25.879Z' }, . . . ] I am looking to retrieve all records where the timestamp is earlier than a ...

Schema-based validation by Joi is recommended for this scenario

How can we apply Joi validation to the schema shown below? What is the process for validating nested objects and arrays in this context? const user = { address: { contactName: 'Sunny', detailAddress: { line1: & ...

What is the process for reaching application-level middleware from the router?

Within my project created using express application generator, I am facing a challenge accessing my application-level middleware from the router. This middleware is specifically designed to query the database using the user ID that is received from the ro ...

Connect-busboy causing NodeJS issue: TypeError - the method 'on' cannot be called on an undefined object

Recently I encountered an issue with a piece of my code: router.route("/post") .get(function(req, res) { // ... }) .post(authReq, function(req, res) { // ... // Get uploaded file var fstream; req.pipe(re ...

Getting the local folder name using AngularJs

Is there a way to retrieve the directory name by browsing to a folder and clicking a button? I was considering utilizing <input type="file" /> to achieve this. ...

Discovering old (potentially neglected) npm dependencies: strategies for locating outdated packages

I am aware of the npm outdated command, which shows out-of-date dependencies only if a new version has been published. However, in certain cases (such as abandoned projects), there may not be a new version released and the package could still be considere ...

Sending a JWT token to a middleware with a GET request in Express Node.js - the proper way

Struggling with Js and web development, I've scoured the web for a solution to no avail... After completing a project for a small lab, my current challenge is creating a login page and generating a web token using JWT... I successfully created a use ...

Encountering a 404 error when attempting to make an Axios post request

Utilizing Axios for fetching data from my backend endpoint has been resulting in a 404 error. Oddly enough, when I manually enter the URI provided in the error message into the browser, it connects successfully and returns an empty object as expected. Her ...