This project is my solution to one of John's Cricket coding challanges. I've build it using NodeJS net module, I choose this option instead of http module to understand more about how servers works and how to properly handle http request.
Project has 3 main files http-server.ts, response.ts and request.ts
The Request class is created after the server reads and parses an incoming request. It stores processed data such as headers, parameters, request body, path, method, etc.
headers: The headers of the incoming request.params: The parameters of the incoming request.body: The body of the incoming request.path: The path of the incoming request.method: The HTTP method of the incoming request.queryParmas: query parameters from incoming request.url: url of the incoming request.httpVersion: httpVersion of the incoming request.
getHeader(): This method reads from theheadersarray and returns the value of a header based on the given key.
The Response class is responsible for setting the statusCode, statusText, and headers of the server's response to incoming requests. It provides methods to set these properties individually or all at once, and it provides several ways to send a response to the client.
headersSent: A boolean that indicates if headers were already sent to the client. Iftrue, an error will be thrown when trying to send headers again, as headers can only be sent once.
setStatusCode(): Sets the status code of the response.setStatusText(): Sets the status text of the response.setHeaders(): Sets the headers of the response.setHead(): A convenience method that sets the status code, status text, and headers all at once.send(): Sends the response to the client. This method takes an optionalbodyargument and decides how to send the response body based on its type. If the body is a string, it sets theContent-Typetotext/html. If the body is an object, boolean, or number, it checks if the body is a buffer. If so, it sets theContent-Typeasapplication/octet-stream. If not, it calls thejson()method. Before sending the response to the client,send()sets theContent-Lengthheader to the appropriate value.json(): Sets theContent-Typetoapplication/json, sets theContent-Lengthheader, and sends the response body in JSON format.sendFile(): Sends an HTML file as a response. This method receives apathargument that is validated by thevalidatePathutility function (only files inside the/src/www/directory are allowed). If the path is valid,sendFile()uses the asynchronousreadFilefunction from Node.js'sfsmodule to first check if the file exists (if not, it sends a 404 response) and then sends the retrieved HTML file withContent-Typeset totext/html.
_sendHeaders(): Checks if headers have already been sent to the client. If so, it throws an error. If not, it sets theheadersSentproperty totrue, sets all previously set headers, and adds aDateheader.getHeader(): A simple method that reads from theheadersarray.
This is the core class of the application. As mentioned before it uses net module from NodeJS to create a TCP server that can handle http requests. It takes two arguments port and host.
host: host address that server is set on.port: port that server is lisening to.server: simplynet.Server()listeners: private property of theHttpServerclass. It is aMapwhere each key is a string that represents a combination of an HTTP method and a path (e.g., 'GET /users'), and each value is an object of typeRouteType.
RouteType is an object that includes:
cb: This is a callback function that gets executed when a request matches the route. It takes two arguments: a request object and a response object.keys: This is an array ofKeyobjects from thepath-to-regexp(borrowed that from express) library. EachKeyrepresents a dynamic part of the path (e.g., in '/users/:id', 'id' is a dynamic part).pathRegex: This is a regular expression generated from the path using thepath-to-regexplibrary. It is used to match incoming requests to the correct route.
init(): method is responsible for initializing the server. It sets up the server to listen on the specified host and port, and configures event handlers for the server and its connections. Init events:error: An event handler is set up for the 'error' event. If the error code indicates that the server is already listening or the address is in use, the port number is incremented and the server attempts to listen again. For any other error, the error is thrown.connection: An event handler is set up for the 'connection' event. This event is emitted whenever a new connection is made to the server.data: Within the 'connection' event handler, another event handler is set up for the 'data' event on the socket. This event is emitted whenever data is received from the client. The data is converted to a string and passed to the_parseRequest()method to create aHttpRequest()object. AHttpResponse()object is also created, and both objects are passed to the_forwardRequestToListener()method.error: Another event handler within the 'connection' event handler is set up for the 'error' event on the socket. If the error code indicates that the connection was reset or the socket is not writable, the connection is ended with a '400 Bad Request' response.
stop(): stops the server.restart(): stops the server and starts to listen on the same port and host.get()andpost()- takes two parameters path and the callback function to execute when a request is received on the specified path. This function should accept two arguments: a request object and a response object. Method first creates an array ofKeyobjects and a regular expression from the path using thepathToRegexpfunction. EachKeyobject represents a dynamic part of the path. It then creates arouteobject that includes the callback function, the array ofKeyobjects, and the regular expression. Finally, it adds therouteobject to thelistenersmap with a key that combines the HTTP method (GET) and the path.
_parseRequest(): This private method is responsible for parsing an incoming request and creating a newHttpRequestobject from the parsed data. Here's how it works:_createParams(): This private method is responsible for creating aparamsobject from the path of the incoming request. It starts by creating an emptyparamsobject. It then finds a matching route from thelistenersmap using the path of the incoming request. If a matching route is found, it executes the route's regular expression against the path to get a match. If a match is found, it iterates over the keys of the matching route. For each key, it adds a new property to theparamsobject where the property name is the key name and the property value is the corresponding matched value from the path. Finally, it returns theparamsobject._parseHeaders(): This private method is responsible for parsing the headers of the incoming request usingreduce()andsplit()funtcions._forwardRequestToListener(): This private method is responsible for forwarding the request and response to the appropriate listener. It starts by finding a matching route from thelistenersmap using the path of the incoming request. If a matching route is found, it calls the callback function of the matching route with the request and response as arguments. If no matching route is found, it sets the response status to '404 Not Found' and sends the response.