DEV Community

Cover image for 56-Nodejs Course 2023: Creating our own Response Class Layer
Hasan Zohdy
Hasan Zohdy

Posted on

 

56-Nodejs Course 2023: Creating our own Response Class Layer

So we have updated our Request class in our previous article to be more organized and we also implemented the request events as well, we need to implement now the response events but there will be a single issue here, that the developer (us or anyone else uses this code) can use the Fastify response object to send the response directly,and does not return anything in the handler.

So we need to stop that from happening, we also need to strict the handler to always return a value, either an object or an array or the our response class.

But let's first create it.

Response Class

Go to src/core/http and create response.ts file and add the following code:

// src/core/http/response.ts

import { FastifyReply } from 'fastify';

export class Response {
  /**
   * Fastify response object
   */
  protected baseResponse: FastifyReply;
}

const response = new Response();

export default response;
Enter fullscreen mode Exit fullscreen mode

As usual, we defined a class and exported it, also we created a new instance for it to be used directly in anywhere in our application.

Now let's add a method that can receive the FastifyReply object and set it to our class property.

// src/core/http/response.ts
import { FastifyReply } from "fastify";

export class Response {
  /**
   * Fastify response object
   */
  protected baseResponse!: FastifyReply;

  /**
   * Set the Fastify response object
   */
  setResponse(response: FastifyReply) {
    this.baseResponse = response;

    return this;
  }
}

const response = new Response();

export default response;
Enter fullscreen mode Exit fullscreen mode

We defined a method that receives Fastify response object and return this for chaining.

Now let's update the request class to use the response class.

// src/core/http/request.ts
import response, { Response } from "./response";
import { FastifyReply, FastifyRequest } from "fastify";

export class Request {
  // ...

  /**
   * Response Object
   * Replace the current one with this
   */
  protected response: Response = response;

  /**
   * Set Fastify response
   */
  public setResponse(response: FastifyReply) {
    this.response.setResponse(response);

    return this;
  }
Enter fullscreen mode Exit fullscreen mode

Here we replaced the fastify response object with our response class, and we also updated the setResponse method to use the setResponse method in our response class.

Response Methods

Now, let's see what methods and properties we need to implement in our response class.

  • status: To set the status code.
  • setContentType: To set the content type.
  • send: To send the response.
  • sendFile: To send a file as a response.
  • success: To send a success response.
  • successCreate: To send a success response with status code 201.
  • badRequest: To send a bad request response with status code 400.
  • notFound: To send a not found response with status code 404.
  • unauthorized: To send an unauthorized response with status code 401.
  • forbidden: To send a forbidden response with status code 403.
  • serverError: To send an error response with status code 500.
  • header: To set the response header.
  • headers: to set multiple headers.
  • getHeaders: To get the response headers.
  • getHeader: To get a specific header.
  • removeHeader: To remove a specific header.
  • getResponseTime: To get the response time.
  • redirect: To redirect the user to another route.
  • on: To listen to a specific event.
  • trigger: To trigger a specific event (protected method).

Too many methods right? But useful onces and we will implement them one by one.

Now let's define the public properties aka the getters that we'll allow:

  • statusCode: To get the status code.
  • contentType: To get the content type.
  • sent: To check if the response is sent or not.

Now let's implement all off them.

status

// src/core/http/response.ts
import { FastifyReply } from "fastify";

export class Response {
  // ...

  /**
   * Set the status code
   */
  public status(statusCode: number) {
    this.baseResponse.status(statusCode);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

setContentType

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Set the content type
   */
  public setContentType(contentType: string) {
    this.baseResponse.header("Content-Type", contentType);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

send

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send the response
   */
  public send(data: any, statusCode = 200) {
    this.baseResponse.status(statusCode).send(data);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

sendFile

// src/core/http/response.ts
import fs from "fs";

export class Response {
  // ...

  /**
   * Send a file as a response
   */
  public sendFile(path: string) {
    if (! fs.existsSync(path)) {
      throw new Error(`Response Send Failed:  File not found: ${path}`);
    }

    const fileContent = fs.readFileSync(path);

    this.baseResponse.send(fileContent);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

success

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send a success response
   */
  public success(data: any) {
    return this.send(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

successCreate

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send a success response with status code 201
   */
  public successCreate(data: any) {
    return this.send(data, 201);
  }
}
Enter fullscreen mode Exit fullscreen mode

badRequest

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send a bad request response with status code 400
   */
  public badRequest(data: any) {
    return this.send(data, 400);
  }
}
Enter fullscreen mode Exit fullscreen mode

notFound

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send a not found response with status code 404
   */
  public notFound(data: any) {
    return this.send(data, 404);
  }
}
Enter fullscreen mode Exit fullscreen mode

unauthorized

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send an unauthorized response with status code 401
   */
  public unauthorized(data: any) {
    return this.send(data, 401);
  }
}
Enter fullscreen mode Exit fullscreen mode

forbidden

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send a forbidden response with status code 403
   */
  public forbidden(data: any) {
    return this.send(data, 403);
  }
}
Enter fullscreen mode Exit fullscreen mode

serverError

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Send an error response with status code 500
   */
  public serverError(data: any) {
    return this.send(data, 500);
  }
}
Enter fullscreen mode Exit fullscreen mode

header

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Set the response header
   */
  public header(key: string, value: string) {
    this.baseResponse.header(key, value);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

headers

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Set multiple headers
   */
  public headers(headers: Record<string, string>) {
    this.baseResponse.headers(headers);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

getHeaders

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Get the response headers
   */
  public getHeaders() {
    return this.baseResponse.getHeaders();
  }
}
Enter fullscreen mode Exit fullscreen mode

getHeader

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Get a specific header
   */
  public getHeader(key: string) {
    return this.baseResponse.getHeader(key);
  }
}
Enter fullscreen mode Exit fullscreen mode

removeHeader

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Remove a specific header
   */
  public removeHeader(key: string) {
    this.baseResponse.removeHeader(key);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

getResponseTime

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Get the response time
   */
  public getResponseTime() {
    return this.baseResponse.getResponseTime();
  }
}
Enter fullscreen mode Exit fullscreen mode

redirect

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Redirect the user to another route
   */
  public redirect(url: string, statusCode = 302) {
    this.baseResponse.redirect(statusCode, url);

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

statusCode

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Get the status code
   */
  public get statusCode() {
    return this.baseResponse.statusCode;
  }
}
Enter fullscreen mode Exit fullscreen mode

contentType

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Get the content type
   */
  public get contentType() {
    return this.baseResponse.getHeader("Content-Type");
  }
}
Enter fullscreen mode Exit fullscreen mode

sent

// src/core/http/response.ts

export class Response {
  // ...

  /**
   * Check if the response has been sent
   */
  public get sent() {
    return this.baseResponse.sent;
  }
}
Enter fullscreen mode Exit fullscreen mode

We've now updated our file with mostly all the methods we need to work with the response. We can now use these methods in our controller to send a response to the user.

Now we need to update the route's handler type to receive the response as second argument and the middleware as well, also the custom validation and make sure to return a proper response.

But first let's define a proper response type for our API.

// src/core/http/types.ts


/**
 * Allowed response type
 */
export type ReturnedResponse =
  /**
   * Can be a response object
   */
  | Response
  /**
   * Or a promise returning a response object
   */
  | Promise<Response>
  /**
   * Or an object
   */
  | Record<string, any>
  /**
   * Or a promise returning an object
   */
  | Promise<Record<string, any>>
  /**
   * Or an array
   */
  | any[]
  /**
   * Or a promise returning an array
   */
  | Promise<any[]>;
Enter fullscreen mode Exit fullscreen mode

Here we allowed three types of response, our response object, or a plain object or an array or a promise returning any of these types, thus the handler can not return anything else.

Now let's update the route's handler type.

// src/core/router/types.ts
import { Request } from "core/http/request";
import { Response } from "core/http/response";
import { ReturnedResponse } from "core/http/types";

/**
 * Middleware method
 * Receives the request and response objects
 * And returns a response object or undefined if the request should continue
 */
export type Middleware = (
  request: Request,
  response: Response,
) => ReturnedResponse | undefined;

/**
 * Route handler receives a request and a response
 * And returns a returning response type
 */
export type RouteHandler = {
  /**
   * Function Declaration
   */
  (request: Request, response: Response): ReturnedResponse;

  /**
   * Validation static object property which can be optional
   */
  validation?: {
    /**
     * Validation rules
     */
    rules?: Record<string, any>;
    /**
     * Validation custom message
     */
    validate?: Middleware;
  };
}

export type RouteOptions = {
  /**
   * Route middleware
   */
  middleware?: Middleware[];
  /**
   * Route name
   */
  name?: string;
};

/**
 * Route Object
 */
export type Route = RouteOptions & {
  /**
   * Route method
   */
  method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
  /**
   * Route path
   */
  path: string;
  /**
   * Route handler
   */
  handler: RouteHandler;
};
Enter fullscreen mode Exit fullscreen mode

We defined a new type RouteHandler as with a function declaration and a static property validation which is optional and can be an object with two properties rules and validate which are also optional.

You will notice that the RouteHandler almost identical to the Middleware type, the only difference is that the Middleware returns a ReturnedResponse or undefined while the RouteHandler returns a ReturnedResponse only.

Now go to the request class and hover on the handler or the middleware, you'll see now its fully typed!!

Now let's update the route methods to accept RouteHandler as the handler as type.

// src/core/router/index.ts

import { Route, RouteHandler, RouteOptions } from "./types";

export class Router {
  // ...
  /**
   * Add get request method
   */
  public get(path: string, handler: RouteHandler, options: RouteOptions = {}) {
    this.routes.push({
      method: "GET",
      path,
      handler,
      ...options,
    });

    return this;
  }

  /**
   * Add post request method
   */
  public post(path: string, handler: RouteHandler, options: RouteOptions = {}) {
    this.routes.push({
      method: "POST",
      path,
      handler,
      ...options,
    });

    return this;
  }

  /**
   * Add put request method
   */
  public put(path: string, handler: RouteHandler, options: RouteOptions = {}) {
    this.routes.push({
      method: "PUT",
      path,
      handler,
      ...options,
    });

    return this;
  }

  /**
   * Add delete request method
   */
  public delete(
    path: string,
    handler: RouteHandler,
    options: RouteOptions = {},
  ) {
    this.routes.push({
      method: "DELETE",
      path,
      handler,
      ...options,
    });

    return this;
  }

  /**
   * Add patch request method
   */
  public patch(
    path: string,
    handler: RouteHandler,
    options: RouteOptions = {},
  ) {
    this.routes.push({
      method: "PATCH",
      path,
      handler,
      ...options,
    });

    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

We updated the route methods to accept RouteHandler as the handler type.

Now our code is ready to be used, our Response class is now more powerful and can adapt the events architecture, this is what we'll do in our next article.

🎨 Conclusion

In this article we've created our Response class to manage how the response will be sent, we've also updated the route's handler type to receive the response as second argument and the middleware as well, also the custom validation and make sure to return a proper response.

🚀 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)

Top comments (0)

We are hiring! Do you want to be our Senior Platform Engineer? Are you capable of chipping in across sysadmin, ops, and site reliability work, while supporting the open source stack that runs DEV and other communities?

This role might just be for you!

Apply now