73-Nodejs Course 2023: Break IV: Refactoring Http Module
Hasan Zohdy
Posted on November 28, 2022
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 tocreateHttpApplication
which makes more sense. - Updating our
types
file to define ourhttpConfigurations
interface. - Creating a new internal config that includes default configurations and an
httpConfig
function to quickly return a configuration fromhttp.*
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";
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
}
}
Now update it in our index.ts
file:
// src/core/http/index.ts
export { default as createHttpApplication } from "./createHttpApplication";
// ...
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();
}
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 () => {
//
},
};
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 prefertype
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[];
};
};
}
As you can tell from above, we just defined what we literally have in our http
configurations.
Note that in the
only
andexcept
themiddleware
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;
};
}
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 = {
//
};
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 behttp.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 tohttp.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;
};
}
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: [],
},
},
};
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));
}
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;
}
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
}
}
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;
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;
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
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
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Posted on November 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.