59-Nodejs Course 2023: Break III: Enhancing Request And Response
Hasan Zohdy
Posted on November 11, 2022
In our previous article, we managed response events and saw how it works and how important it is, now let's enhance our http request and response classes.
Removing validate method
As the validation process is related to validator, then let's move the validate method to the validator folder, we can actually make a helper function let's call it validateAll
this function will validate the validation rules and the custom validation function (if exists) and return the result.
// src/core/validator/validateAll.ts
import config from "@mongez/config";
import { Request } from "core/http/request";
import { Response } from "core/http/response";
import { Route } from "core/router/types";
import Validator from "./validator";
/**
* Validate the request route
*/
export default async function validateAll(
validation: Route["handler"]["validation"],
request: Request,
response: Response,
) {
if (validation?.rules) {
const validator = new Validator(request, validation.rules);
try {
await validator.scan(); // start scanning the rules
} catch (error) {
console.log(error);
}
if (validator.fails()) {
const responseErrorsKey = config.get(
"validation.keys.response",
"errors",
);
const responseStatus = config.get("validation.responseStatus", 400);
return response.send(
{
[responseErrorsKey]: validator.errors(),
},
responseStatus,
);
}
}
if (validation?.validate) {
const result = await validation.validate(request, response);
if (result) {
return result;
}
}
}
What we did is we moved the validate
method body to the method, we set the validation type to be Route.handler.validation
type and passed the request and response classes, now let's import it in our request class.
But first let's update the validator index to export the validateAll
function.
// src/core/validator/index.ts
export { default as validateAll } from "./validateAll";
// ...
Now updating the request class.
// src/core/http/request.ts
import { validateAll } from "core/validator";
// ...
/**
* Execute the request
*/
public async execute() {
// check for middleware first
const middlewareOutput = await this.executeMiddleware();
if (middlewareOutput !== undefined) {
return middlewareOutput;
}
const handler = this.route.handler;
// 👇🏻 check for validation using validateAll helper function
const validationOutput = await validateAll(
handler.validation,
this,
this.response,
);
if (validationOutput !== undefined) {
return validationOutput;
}
// call executingAction event
this.trigger("executingAction", this.route);
const output = await handler(this, this.response);
// call executedAction event
this.trigger("executedAction", this.route);
return output;
}
// Don't forget to remove validate method
Now we moved the validate method to validator and got the result from it, if the result is not undefined
then we return it, otherwise we continue with the request execution.
We've added the event trigger to validation class for the validation rules, but we didn't add it to the custom validation function, so let's add the custom validation
event trigger.
Let's add thecustomValidating
customPasses
customFails
customDone
events to ValidatioNEvent
type.
// src/core/validator/types.ts
/**
* Validation event types
*/
export type ValidationEvent =
| "validating"
| "passes"
| "fails"
| "done"
| "customValidating"
| "customPasses"
| "customFails"
| "customDone"
Now le'ts update Validator.trigger
to be public to trigger the custom validation events.
// src/core/validator/validator.ts
/**
* Trigger an event
*/
public trigger(event: ValidationEvent, ...args: any[]) {
this.events.trigger(event, ...args);
}
Updating validateAll
function
// src/core/validator/validateAll.ts
import config from "@mongez/config";
import { Request } from "core/http/request";
import { Response } from "core/http/response";
import { Route } from "core/router/types";
import Validator from "./validator";
/**
* Validate the request route
*/
export default async function validateAll(
validation: Route["handler"]["validation"],
request: Request,
response: Response,
) {
if (validation?.rules) {
const validator = new Validator(request, validation.rules);
try {
await validator.scan(); // start scanning the rules
} catch (error) {
console.log(error);
}
if (validator.fails()) {
const responseErrorsKey = config.get(
"validation.keys.response",
"errors",
);
const responseStatus = config.get("validation.responseStatus", 400);
return response.send(
{
[responseErrorsKey]: validator.errors(),
},
responseStatus,
);
}
}
if (validation?.validate) {
Validator.trigger("customValidating", validation.validate);
const result = await validation.validate(request, response);
Validator.trigger("customDone", result);
// if there is a result, it means it failed
if (result) {
Validator.trigger("customFails", result);
// check if there is no response status code, then set it to config value or 400 as default
if (!response.statusCode) {
response.setStatusCode(config.get("validation.responseStatus", 400));
}
return result;
}
Validator.trigger("customPasses");
}
}
We added the events as mentioned earlier, but i also added the response status code check step, as if the custom validation did not set response code, then we need to set it internally to the config value or 400
as default.
Capturing Response Body
Now we can not tell if we have the response body set or not, so let's add a property called body
that will hold the response body when set in the send
method.
// src/core/http/response.ts
export class Response {
// ...
/**
* Current response body
*/
protected currentBody: any;
/**
* Get Current response body
*/
public get body() {
return this.currentBody;
}
}
Now let's capture it in the send
method.
// src/core/http/response.ts
/**
* Send the response
*/
public send(body: any, statusCode?: number) {
this.currentBody = body;
// ...
}
Reset response data on response send
Now imagine that you have set the response body in the /
route, now the user has navigated to /users
, if you tried to log response.body
you will see that there is a value, so we need to reset the response body on each response send, we can use Fastify onResponse hook as it is called after each response is sent to clear the response body, route and current status code.
In that case, we 're going to visit connectToServer.ts
file to listen to it.
// src/core/http/connectToServer.ts
import multipart from "@fastify/multipart";
import config from "@mongez/config";
import router from "core/router";
import Fastify from "fastify";
import response from "./response";
export default async function connectToServer() {
const server = Fastify();
server.register(multipart, {
attachFieldsToBody: true,
});
// 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
}
}
We added a new hook (Event) to Fastify object, that when the onResponse
event is triggered, it will call the reset
method of the response object.
Now let's implement the reset
method.
// src/core/http/response.ts
/**
* Reset the response state
*/
public reset() {
this.route = undefined;
this.currentBody = null;
this.currentStatusCode = 200;
}
Now the typescript will start complaining, because we have not defined the route
property, so let's update it to be optional.
// src/core/http/response.ts
export class Response {
// ...
/**
* Current route
*/
protected route?: Route;
// ...
}
Caching Request Payload
Caching request payload will decrease the process we do on the request inputs such as parseInputValue
, so we need to do it once the setRequest
method receives the new request.
// src/core/http/response.ts
/**
* Parsed Request Payload
*/
protected payload: any = {};
/**
* Set request handler
*/
public setRequest(request: FastifyRequest) {
this.request = request;
this.parsePayload();
return this;
}
Now let's implement the parsePayload
method.
// src/core/http/response.ts
/**
* Parse the payload and merge it from the request body, params and query string
*/
protected parsePayload() {
this.payload.body = this.parseBody();
this.payload.query = this.request.query;
this.payload.params = this.request.params;
this.payload.all = {
...this.payload.body,
...this.payload.query,
...this.payload.params,
};
}
/**
* Parse body payload
*/
private parseBody() {
const body: any = {};
for (const key in this.request.body) {
const keyData = this.request.body[key];
if (Array.isArray(keyData)) {
body[key] = keyData.map(this.parseInputValue.bind(this));
} else {
body[key] = this.parseInputValue(keyData);
}
}
return body;
}
/**
* Update the body getter to receive the request body from payload body property
* Get request body
*/
public get body() {
return this.payload.body;
}
/**
* Get all inputs
*/
public all() {
return this.payload.all;
}
The payload object will hold four objects, the body
, query
, params
and all
which is the combination of all of them.
The body
object will hold the parsed body, the query
object will hold the query string, the params
object will hold the route params and the all
object will hold the combination of all of them.
Now we can use the all
method to get all inputs using payload.all
instead of payload.body
, payload.query
and payload.params
.
Updating input method
Now le'ts enhance the input method to take the value from payload.all
property, but also we need to accept dot.notation
syntax.
import { get } from '@mongez/reinforcements';
// ...
/**
* Get request input value from query string, params or body
*/
public input(key: string, defaultValue: any = null) {
return get(this.payload.all, key, defaultValue);
}
We used get
method from @mongez/reinforcements
package to get the value from the object using dot.notation
syntax.
Now we can get a value from request input using dot notation.
const username = response.input('user.name', 'Hasan');
And that's it for now!
🎨 Conclusion
In this article, we have enhanced our request class, moved validation process in a separated function and added custom validation events.
We also added a response body
property to store current body and added a reset
method to reset response data.
Later we enhanced the request payload and made it more controlled and performant.
🚀 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 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.