56-Nodejs Course 2023: Creating our own Response Class Layer
Hasan Zohdy
Posted on November 10, 2022
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;
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;
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;
}
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;
}
}
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;
}
}
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;
}
}
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;
}
}
success
// src/core/http/response.ts
export class Response {
// ...
/**
* Send a success response
*/
public success(data: any) {
return this.send(data);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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;
}
}
headers
// src/core/http/response.ts
export class Response {
// ...
/**
* Set multiple headers
*/
public headers(headers: Record<string, string>) {
this.baseResponse.headers(headers);
return this;
}
}
getHeaders
// src/core/http/response.ts
export class Response {
// ...
/**
* Get the response headers
*/
public getHeaders() {
return this.baseResponse.getHeaders();
}
}
getHeader
// src/core/http/response.ts
export class Response {
// ...
/**
* Get a specific header
*/
public getHeader(key: string) {
return this.baseResponse.getHeader(key);
}
}
removeHeader
// src/core/http/response.ts
export class Response {
// ...
/**
* Remove a specific header
*/
public removeHeader(key: string) {
this.baseResponse.removeHeader(key);
return this;
}
}
getResponseTime
// src/core/http/response.ts
export class Response {
// ...
/**
* Get the response time
*/
public getResponseTime() {
return this.baseResponse.getResponseTime();
}
}
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;
}
}
statusCode
// src/core/http/response.ts
export class Response {
// ...
/**
* Get the status code
*/
public get statusCode() {
return this.baseResponse.statusCode;
}
}
contentType
// src/core/http/response.ts
export class Response {
// ...
/**
* Get the content type
*/
public get contentType() {
return this.baseResponse.getHeader("Content-Type");
}
}
sent
// src/core/http/response.ts
export class Response {
// ...
/**
* Check if the response has been sent
*/
public get sent() {
return this.baseResponse.sent;
}
}
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[]>;
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;
};
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;
}
}
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
- 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 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.