59-Nodejs Course 2023: Break III: Enhancing Request And Response

hassanzohdy

Hasan Zohdy

Posted on November 11, 2022

59-Nodejs Course 2023: Break III: Enhancing Request And Response

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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";
// ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;

    // ...
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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

Packages & Libraries

React Js Packages

Courses (Articles)

💖 💪 🙅 🚩
hassanzohdy
Hasan Zohdy

Posted on November 11, 2022

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

Sign up to receive the latest update from our blog.

Related