73-Nodejs Course 2023: Break IV: Refactoring Http Module

hassanzohdy

Hasan Zohdy

Posted on November 28, 2022

73-Nodejs Course 2023: Break IV: Refactoring Http Module

After our previous Break III we have added new features, and enhanced our existing onces, we added Middleware concept, and made our application a little more secure using throttling and Bloody Cors, we also created our new Auth module and worked with JWT.

So we've done a lot of work here since then, now we need to clean up our code.

📜 The Plan

Our http folder is a total mess, we have a lot of files, and we need to clean it up, we need to move some files and folders, so here what we are going to do:

  • Updating index.ts to encapsulate everything to be exported from it.
  • Renaming connectToServer function to createHttpApplication which makes more sense.
  • Updating our types file to define our httpConfigurations interface.
  • Creating a new internal config that includes default configurations and an httpConfig function to quickly return a configuration from http.* configurations.

That's in my head right now, so let's start.

📝 Updating index.ts

Open src/core/http/index.ts and update it to look like this:

// src/core/http/index.ts
export { default as connectToServer } from "./connectToServer";
// request exports
export * from "./request";
// we need to make a default export from request. ts but to be
// exported as request not as default export
export { default as request } from "./request";
// response exports
export * from "./response";
// same thing like request
export { default as response } from "./response";
// server exports, but not recommended to beb used outside this folder
export * from "./server";
// types
export * from "./types";
// Uploaded file
export { default as UploadedFile } from "./UploadedFile";
Enter fullscreen mode Exit fullscreen mode

Nothing fancy, we just exported all of our internal files to be exported from one place, now let's rename our main function.

📝 Renaming connectToServer function

As i said earlier, connectToServer function name doesn't make sense, so let's rename it to createHttpApplication which makes more sense.

Open src/core/http/connectToServer.ts and rename it to createHttpApplication.ts and update it to look like this:

// src/core/http/createHttpApplication.ts
import config from "@mongez/config";
import router from "core/router";
import registerHttpPlugins from "./plugins";
import response from "./response";
import { getServer } from "./server";

export default async function createHttpApplication() {
  const server = getServer();

  await registerHttpPlugins();

  // call reset method on response object to response its state
  server.addHook("onResponse", response.reset.bind(response));

  router.scan(server);

  try {
    // 👇🏻 We can use the url of the server
    const address = await server.listen({
      port: config.get("app.port"),
      host: config.get("app.baseUrl"),
    });

    console.log(`Start browsing using ${address}`);
  } catch (err) {
    console.log(err);

    server.log.error(err);
    process.exit(1); // stop the process, exit with error
  }
}
Enter fullscreen mode Exit fullscreen mode

Now update it in our index.ts file:

// src/core/http/index.ts
export { default as createHttpApplication } from "./createHttpApplication";
// ...
Enter fullscreen mode Exit fullscreen mode

Now let's update our src/core/application.ts to use our new function:

// src/core/application.ts
import { connectToDatabase } from "core/database";
import { createHttpApplication } from "core/http";

export default async function startApplication() {
  connectToDatabase();
  createHttpApplication();
}
Enter fullscreen mode Exit fullscreen mode

Simple enough, right?

Now let's update all of our routes that we used a direct import for request and response types.

// src/app/users/controllers/create-user.ts
import { Request } from "core/http";

export default async function createUser(request: Request) {
  const { name, email } = request.body;

  return {
    name,
    email,
  };
}

createUser.validation = {
  rules: {
    name: ["required", "string"],
    email: ["required", "string"],
  },
  validate: async () => {
    //
  },
};
Enter fullscreen mode Exit fullscreen mode

We replaced import { Request } from 'core/http/request' to import { Request } from 'core/http' which is the same thing.

Now go through all your files and update it as i did above.

You can easily find it in your IDE by searching for core/http/ and it will show you all files that uses request and response imports.

Also if you found something like this import request from 'core/http/request' just replace it with import { request } from 'core/http'.

📝 Updating our types file

Now let's add our httpConfigurations interface to our types file.

I'm going to use interface this time just so you can be familiar with both, i prefer type though but it's up to you.

// src/core/http/types.ts
// ...

/**
 * Http Configurations list
 */
export interface HttpConfigurations {
  /**
   * Http middlewares list
   */
  middleware?: {
    /**
     * All middlewares that are passed to `all` array will be applied to all routes
     */
    all?: Middleware[];
    /**
     * Middlewares that are passed to `only` object will be applied to specific routes
     */
    only?: {
      /**
       * Routes list
       * @example routes: ["/users", "/posts"]
       */
      routes?: string[];
      /**
       * Named routes list
       *
       * @example namedRoutes: ["users.list", "posts.list"]
       */
      namedRoutes?: string[];
      /**
       * Middlewares list
       */
      middleware: Middleware[];
    };
    /**
     * Middlewares that are passed to `except` object will be excluded from specific routes
     */
    except?: {
      /**
       * Routes list
       * @example routes: ["/users", "/posts"]
       */
      routes?: string[];
      /**
       * Named routes list
       *
       * @example namedRoutes: ["users.list", "posts.list"]
       */
      namedRoutes?: string[];
      /**
       * Middlewares list
       */
      middleware: Middleware[];
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

As you can tell from above, we just defined what we literally have in our http configurations.

Note that in the only and except the middleware property is required, so you can't just pass an empty object.

Also, if you notice that only and except has the same definition, so let's create an interface for it.

// src/core/http/types.ts
// ...

/**
 * Partial Middleware
 */
export interface PartialMiddleware {
  /**
   * Routes list
   * @example routes: ["/users", "/posts"]
   */
  routes?: string[];
  /**
   * Named routes list
   *
   * @example namedRoutes: ["users.list", "posts.list"]
   */
  namedRoutes?: string[];
  /**
   * Middlewares list
   */
  middleware: Middleware[];
}

/**
 * Http Configurations list
 */
export interface HttpConfigurations {
  /**
   * Http middlewares list
   */
  middleware?: {
    /**
     * All middlewares that are passed to `all` array will be applied to all routes
     */
    all?: Middleware[];
    /**
     * Middlewares that are passed to `only` object will be applied to specific routes
     */
    only?: PartialMiddleware;
    /**
     * Middlewares that are passed to `except` object will be excluded from specific routes
     */
    except?: PartialMiddleware;
  };
}
Enter fullscreen mode Exit fullscreen mode

Now our code is much cleaner and easier to maintain later.

📝 Creating internal config file

The purpose of this file is to easily manage our http configurations, so we can get the http config and its default value directly.

// src/core/http/config.ts
import { HttpConfigurations } from "./types";

/**
 * Default http configurations
 */
export const defaultHttpConfigurations: HttpConfigurations = {
  // 
};
Enter fullscreen mode Exit fullscreen mode

We added just an empty object for now, but what configurations should be added more besides the middleware? Let's see.

An easy and quick way to do so, is Right click on the http directory and click on Find in Folder and search for config.get and it will show you all files that uses the http configurations.

Which are the following:

  • app.port: we'll replace it to be http.port as it is just related to the http application, (For example sockets have their own port)
  • app.baseUrl: this one will also be replaced to http.baseUrl as it is just related to the http application.
  • http.middleware.all: our new middleware configurations that collects all middlewares that are applied to all routes.
  • http.middleware.only: our new middleware configurations that collects all middlewares that are applied to specific routes.
  • http.middleware.except: our new middleware configurations that collects all middlewares that are excluded from specific routes.

So we've here 4 configurations that we need to add to our defaultHttpConfigurations object.

We added the middleware in http configurations type, let's add the port type

// src/core/http/types.ts
// ...

/**
 * Http Configurations list
 */
export interface HttpConfigurations {
  /**
   * Server port
   */
  port?: number;
  /**
   * Host
   */
  host?: string;
  /**
   * Http middlewares list
   */
  middleware?: {
    /**
     * All middlewares that are passed to `all` array will be applied to all routes
     */
    all?: Middleware[];
    /**
     * Middlewares that are passed to `only` object will be applied to specific routes
     */
    only?: PartialMiddleware;
    /**
     * Middlewares that are passed to `except` object will be excluded from specific routes
     */
    except?: PartialMiddleware;
  };
}
Enter fullscreen mode Exit fullscreen mode

Now let's add the port to our defaultHttpConfigurations object to be default to 3000 and the host to be default to 0.0.0.0.

Why would we set host to 0.0.0.0? based on Fastify documentation it can be used to listen to any host if we don't want to specify a specific host.

// src/core/http/config.ts
import { HttpConfigurations } from "./types";

/**
 * Default http configurations
 */
export const defaultHttpConfigurations: HttpConfigurations = {
  port: 3000,  
  host: "0.0.0.0",
  middleware: {
    all: [],
    only: {
      middleware: [],
    },
    except: {
      middleware: [],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

I listed here the port, and all the middlewares as empty arrays.

📝 Creating the httpConfig function

Now we have our defaultHttpConfigurations object, let's create a function that will return the http configurations.

// src/core/http/config.ts
import config from "@mongez/config";
import { get } from "@mongez/reinforcements";
import { HttpConfigurations } from "./types";

/**
 * Default http configurations
 */
export const defaultHttpConfigurations: HttpConfigurations = {
  port: 3000,
  middleware: {
    all: [],
    only: {
      middleware: [],
    },
    except: {
      middleware: [],
    },
  },
};

/**
 * Get http configurations for the given key
 */
export function httpConfig(key: string): any {
  return config.get(`http.${key}`, get(defaultHttpConfigurations, key));
}
Enter fullscreen mode Exit fullscreen mode

Here we used the config.get function to get the http configurations, and if it doesn't exist, we'll return the default value from the defaultHttpConfigurations using get utility to return a nested key's value.

📝 Using the httpConfig function

Now let's update our code to use our new httpConfig function.

// src/core/http/request.ts
import { httpConfig } from "./config";
//...

  /**
   * Collect middlewares for current route
   */
  protected collectMiddlewares(): Middleware[] {
    // we'll collect middlewares from 4 places
    // We'll collect from http configurations under `http.middleware` config
    // it has 3 middlewares types, `all` `only` and `except`
    // and the final one will be the middlewares in the route itself
    // so the order of collecting and executing will be: `all` `only` `except` and `route`
    const middlewaresList: Middleware[] = [];

    // 1- collect all middlewares as they will be executed first 👇🏻 we used our new `httpConfig` function
    const allMiddlewaresConfigurations = httpConfig("middleware.all");

    // check if it has middleware list
    if (allMiddlewaresConfigurations?.middleware) {
      // now just push everything there
      middlewaresList.push(...allMiddlewaresConfigurations.middleware);
    }

    // 2- check if there is `only` property 👇🏻 we used our new `httpConfig` function
    const onlyMiddlewaresConfigurations = httpConfig("middleware.only");

    if (onlyMiddlewaresConfigurations?.middleware) {
      // check if current route exists in the `routes` property
      // or the route has a name and exists in `namedRoutes` property
      if (
        onlyMiddlewaresConfigurations.routes?.includes(this.route.path) ||
        (this.route.name &&
          onlyMiddlewaresConfigurations.namedRoutes?.includes(this.route.name))
      ) {
        middlewaresList.push(...onlyMiddlewaresConfigurations.middleware);
      }
    }

    // 3- collect routes from except middlewares 👇🏻 we used our new `httpConfig` function
    const exceptMiddlewaresConfigurations = httpConfig("middleware.except");

    if (exceptMiddlewaresConfigurations?.middleware) {
      // first check if there is `routes` property and route path is not listed there
      // then check if route has name and that name is not listed in `namedRoutes` property
      if (
        !exceptMiddlewaresConfigurations.routes?.includes(this.route.path) &&
        this.route.name &&
        !exceptMiddlewaresConfigurations.namedRoutes?.includes(this.route.name)
      ) {
        middlewaresList.push(...exceptMiddlewaresConfigurations.middleware);
      }
    }

    // 4- collect routes from route middlewares
    if (this.route.middleware) {
      middlewaresList.push(...this.route.middleware);
    }

    return middlewaresList;
  }
Enter fullscreen mode Exit fullscreen mode

We just used our new httpConfig function to get the http configurations instead of using the config handler directly, this will make our code more readable and easier to maintain.

Now let's update our createHttpApplication function to use our new httpConfig function.

// src/core/http/createHttpApplication.ts
import router from "core/router";
import { httpConfig } from "./config";
import registerHttpPlugins from "./plugins";
import response from "./response";
import { getServer } from "./server";

export default async function createHttpApplication() {
  const server = getServer();

  await registerHttpPlugins();

  // call reset method on response object to response its state
  server.addHook("onResponse", response.reset.bind(response));

  router.scan(server);

  try {
    // 👇🏻 We can use the url of the server
    const address = await server.listen({
      port: httpConfig("port"),
      host: httpConfig("host"),
    });

    console.log(`Start browsing using ${address}`);
  } catch (err) {
    console.log(err);

    server.log.error(err);
    process.exit(1); // stop the process, exit with error
  }
}
Enter fullscreen mode Exit fullscreen mode

We just used our new httpConfig function to get the http configurations instead of using the config handler directly, this will make our code more readable and easier to maintain.

Now we need to move our app.port and app.baseUrl to the http configurations.

// src/config/http.ts
import { env } from "@mongez/dotenv";
import { authMiddleware } from "core/auth/auth-middleware";
import { HttpConfigurations } from "core/http";

const httpConfigurations: HttpConfigurations = {
  port: env("PORT", 3000),
  host: env("HOST", "localhost"),
  middleware: {
    // apply the middleware to all routes
    all: [],
    // apply the middleware to specific routes
    only: {
      routes: [],
      namedRoutes: ["users.list"],
      middleware: [authMiddleware("guest")],
    },
    // exclude the middleware from specific routes
    except: {
      routes: [],
      namedRoutes: [],
      middleware: [],
    },
  },
};

export default httpConfigurations;
Enter fullscreen mode Exit fullscreen mode

Now our app.ts config file has become very simple.

// src/config/app.ts
import { env } from "@mongez/dotenv";

const appConfigurations = {
  debug: env("DEBUG", false),
};

export default appConfigurations;
Enter fullscreen mode Exit fullscreen mode

Final thing to update is our .env file to replace BASE_URL with HOST

# App Configurations
DEBUG=true
APP_NAME="My App"

# Http Configurations
PORT=3000
HOST=localhost

# Database Configurations
DB_HOST=localhost
DB_PORT=27017
DB_NAME=ninjaNode
DB_USERNAME=root
DB_PASSWORD=root
Enter fullscreen mode Exit fullscreen mode

Writing comments inside a file will help you understand and more importantly, remember what are these keys will be going to used for.

🎨 Conclusion

Wheoh, we've made such a brilliant cleanup here, the http module is now much better, we can refine it more though 😜 but we're good for now.

I hope you enjoyed this article, and I hope you learned something new, if you have any questions or suggestions, please let me know in the comments below.

One final note, we may do such a repeated code refactoring to the same module as much as we enhance it and add new features, this is a healthy thing to your codebase.

☕♨️ Buy me a Coffee ♨️☕

If you enjoy my articles and see it useful to you, you may buy me a coffee, it will help me to keep going and keep creating more content.

🚀 Project Repository

You can find the latest updates of this project on Github

😍 Join our community

Join our community on Discord to get help and support (Node Js 2023 Channel).

🎞️ Video Course (Arabic Voice)

If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.

📚 Bonus Content 📚

You may have a look at these articles, it will definitely boost your knowledge and productivity.

General Topics

Packages & Libraries

React Js Packages

Courses (Articles)

💖 💪 🙅 🚩
hassanzohdy
Hasan Zohdy

Posted on November 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related