DEV Community

Cover image for 85-Nodejs Course 2023: Response: Async Body Parser
Hasan Zohdy
Hasan Zohdy

Posted on

85-Nodejs Course 2023: Response: Async Body Parser

In our previous chapter, we saw how to use our resource and added couple features boot and extend which led us to make our toJSON method to be async, that's where We got a serious issue, as JSON.stringify doesn't support async functions, so we need to find a way to make it work.

Async Json Parser

Remember our response class? yes that one that we didn't use it yet, it's time to use it now, we'll use it to parse the data before sending it to the response, but before updating it, we need to update our request class to pass the output of the handler to the response class.

// src/core/http/request.ts

// ...

  /**
   * Execute the request
   */
  public async execute() {
    // check for middleware first
    const middlewareOutput = await this.executeMiddleware();

    if (middlewareOutput !== undefined) {
      // 👇🏻 send the response
      return this.response.send(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) {
      // 👇🏻 send the response
      return this.response.send(validationOutput);
    }

    // call executingAction event
    this.trigger("executingAction", this.route);
    const output = await handler(this, this.response);

    // call executedAction event
    this.trigger("executedAction", this.route);

    // 👇🏻 send the response
    await this.response.send(output);
  }
Enter fullscreen mode Exit fullscreen mode

So instead of returned the output from middleware, validation or the handler, we are going to pass that output to the response's send method so we can handle that output freely.

Now let's update our response class to handle the output.

// src/core/http/response.ts
// ...

  /**
   * Send the response
   */
  public async send(data?: any, statusCode?: number) {
    if (data) {
      this.currentBody = data;
    }

    if (statusCode) {
      this.currentStatusCode = statusCode;
    }

    if (!this.currentStatusCode) {
      this.currentStatusCode = 200;
    }

    // trigger the sending event
    this.trigger("sending", this.currentStatusCode, data);

    this.baseResponse.status(this.currentStatusCode).send(data);

    // trigger the sent event
    this.trigger("sent", this.currentStatusCode, data);

    // trigger the success event if the status code is 2xx
    if (this.currentStatusCode >= 200 && this.currentStatusCode < 300) {
      this.trigger("success", data, this.currentStatusCode, this.route);
    }

    // trigger the successCreate event if the status code is 201
    if (this.currentStatusCode === 201) {
      this.trigger("successCreate", data, this.currentStatusCode, this.route);
    }

    // trigger the badRequest event if the status code is 400
    if (this.currentStatusCode === 400) {
      this.trigger("badRequest", data, this.currentStatusCode, this.route);
    }

    // trigger the unauthorized event if the status code is 401
    if (this.currentStatusCode === 401) {
      this.trigger("unauthorized", data, this.currentStatusCode, this.route);
    }

    // trigger the forbidden event if the status code is 403
    if (this.currentStatusCode === 403) {
      this.trigger("forbidden", data, this.currentStatusCode, this.route);
    }

    // trigger the notFound event if the status code is 404
    if (this.currentStatusCode === 404) {
      this.trigger("notFound", data, this.currentStatusCode, this.route);
    }

    // trigger the throttled event if the status code is 429
    if (this.currentStatusCode === 429) {
      this.trigger("throttled", data, this.currentStatusCode, this.route);
    }

    // trigger the serverError event if the status code is 500
    if (this.currentStatusCode === 500) {
      this.trigger("serverError", data, this.currentStatusCode, this.route);
    }

    // trigger the error event if the status code is 4xx or 5xx
    if (this.currentStatusCode >= 400) {
      this.trigger("error", data, this.currentStatusCode, this.route);
    }

    return this;
  }
Enter fullscreen mode Exit fullscreen mode

This is our previous send method, we're going to add a new line after that first check.

// src/core/http/response.ts

// ...

  /**
   * Send the response
   */
  public async send(data?: any, statusCode?: number) {
    if (data) {
      this.currentBody = data;
    }

    // parse the body and make sure it is transformed to sync data instead of async data
    data = await this.parseBody();
    // ...
  }
Enter fullscreen mode Exit fullscreen mode

We added a new line here, which is going to parse our currentBody data, and make sure it's transformed to sync data instead of async data, so we can send it to the response.

Body Parser

Now let's create our parseBody method, but before we go to the implementation let me tell you the workflow of this method.

Basically, this method will call another method called parse and pass the currentBody to that method.

Why would we do this? because we're going in some recursion here, so the parse method will receive any kind of data and handle it accordingly regardless its the main body or a nested object.

Now returning to the parse method workflow

  1. If the given value is a falsy value, we'll return it as it is.
  2. Check if the passed data has toJSON method, if so then await it and return the result.
  3. Check if the passed data is an array, if so then loop over it and call the parse method on each item and return the result.
  4. If it is not plain object and not an array and does not have toJSON then we're going to just return it.
  5. If it is a plain object then we're going to loop over its keys and call the parse method on each value and return the result.

Actually we're going to change the point of array, we're going to check if the value is iterable instead of checking if it is an array, because we want to support any kind of iterable data, not just arrays.

Now let's go to the implementation.

// src/core/http/response.ts
// we'll need it here to make couple checks later
import Is from "@mongez/supportive-is";

// ...
export class Response {
    // ...
    /**
     * Parse body
     */
    protected async parseBody() {
        return await this.parse(this.currentBody);
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Now let's create the parse method.

// src/core/http/response.ts
// ...


export class Response {
    // ...

  /**
   * Parse the given value
   */
  protected async parse(value: any): Promise<any> {
    // if it is a falsy value, return it
    if (!value) return value;

    // if it has a `toJSON` method, call it and await the result then return it
    if (value.toJSON) {
      return await value.toJSON();
    }

    // if it is iterable, an array or array-like object then parse each item
    if (Is.iterable(value)) {
      return await Promise.all(
        Array.from(value).map((item: any) => {
          return this.parse(item);
        }),
      );
    }

    // if not plain object, then return it
    if (!Is.plainObject(value)) {
      return value;
    }

    // loop over the object and check if the value and call `parse` on it
    for (const key in value) {
      const subValue = value[key];

      value[key] = await this.parse(subValue);
    }

    return value;
  }
}
Enter fullscreen mode Exit fullscreen mode

The code is pretty much as the flow steps i wrote above, we checked if its a falsy value then we returned it, if it has a toJSON method then await the call of it and return it.

If it is iterable (That's why we imported Supportive Is method) then we're going to loop over it and call the parse method on each item and return the result.

If it is not plainObject then we're going to return it.

If it is a plainObject then we're going to loop over its keys and call the parse method on each value and return the result.

Regarding the iterable data, you might see the code a little weird, we used Promise.all to await all promises to be resolved, that Promise.all method receives an array, that's why we used Array.from to convert the iterable data (arrays are also considered iterable) to an array, then map each value to be called with the parse method.

Now if we tried again returning the resource, it will work just fine.

🎨 Conclusion

In this section, we customized the final output of the response, we make our own data parser to make every async data to be sent as sync data.

☕♨️ 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)

Latest comments (0)