Step-by-Step Guide to Setting Up Your Own HTTP Server with JavaScript
Table of contents
- This is Step 1: Binding to a Port
- Step 2: Respond with 200
- Step 3 : Respond with 404
- Step 5: Parse Headers
- Step-6: Concurrent connections
- Step-7 : Get A File and Post a File
- By following this code and the explanations provided throughout the blog, you should be able to create a fully functional file server that meets the specified requirements. Do checkout the Challenge by CodeCrafters
So I was completing this challenge from CODECRAFTERS, and thought why not Document whole journey and I think It will help me understanding everything in a very better manner.
This is Step 1: Binding to a Port
Before Starting anything, You might ask, hey Ankit, What is this Bind to a PORT? What Does this even me?
So let's understand it this way
Imagine you're running a business that provides some service (like a restaurant or a store). The first step in setting up your business is to find a physical location or address where customers can come and access your service.
In the context of creating an HTTP server, binding to a port is like finding that physical address for your business.
When you create an HTTP server using Node.js (or any other programming language), you need to tell the server which
"address"
(combination of IP address and port number) it should listen on for incoming requests from clients (web browsers, mobile apps, etc.).The IP address is like the street address or the city where your business is located. It identifies the specific machine (computer or server) where your HTTP server is running.
The port number is like the suite number or the floor number within that building or address. It helps distinguish your service (the HTTP server) from other services that might be running on the same machine.
So, when you bind your HTTP server to a specific IP address and port number, you're essentially saying, "My server will be located at this address (IP) and will operate on this floor (port number)."
For example, if you bind your HTTP server to
localhost
(IP address127.0.0.1
) and port3000
, you're telling it to listen for incoming requests at the address127.0.0.1:3000
.
Now, The Code for Binding to a Port
const net = require("net");
console.log("Logs from your program will appear here!");
// Btw This code below was pre-written on codecrafters repo which i cloned
const server = net.createServer((socket) => {
socket.on("close", () => {
socket.end();
server.close();
});
});
server.listen(4221, "localhost");
uffffffffff!!!! what is this all written Ankit?? Can you explain it please?
Yes I am there to help you, don't worry...
Now let's break the code line by line
const net = require("net");
net module is used to create TCP servers and clients
const server = net.createServer((socket) => {
socket.on("close", () => {
socket.end();
server.close();
});
});
createServer
which takes a callback function and it allows you to establish a listening endpoint on your system that can accept incoming connections from clients (other applications or devices). For now, we're just setting up a listener for the "close"
event on the client socket, which will end the connection and close the server when the client disconnects.server.listen(4221, "localhost");
// This line is self explainatory, we are setting our server to listen on
// localhost:4221 (address)
Step 2: Respond with 200
In this step, we'll respond to a HTTP request with a 200 OK response
.
The Task is to:
Accept a TCP connection
Read data from the connection (we'll get to parsing it in later stages)
Respond with
HTTP/1.1 200 OK\r\n\r\n
So how to do that? Now we will go into it step-by-step
We created a TCP connection in Step 1(check above), now the two remaining steps are reading a data and then responding according to it.
so how to that?
socket.on("data" , () => { socket.write("HTTP/1.1 200 OK\r\n\r\n"); })
socket.on("data" , ()=>{})
this method here, receives the data from the connection and then calls a callback function ,socket.write()
which sends this specific response which was asked in the step, which specifies the following thing:HTTP/1.1
is protocol version200
is the status codeOK
is(according to MDN): A status text. A brief, purely informational, textual description of the status code to help a human understand the HTTP message.\r\n
: The MDN defines it asCR and LF are control characters or bytecode thatcan be used to mark a line break in a text file.
CR = Carriage Return (
\r
,0x0D
in hexadecimal, 13in decimal) โ moves the cursor to the beginning of the line without advancing to the next line.LF = Line Feed (
\n
,0x0A
in hexadecimal, 10 in decimal) โ moves the cursor down to the next line without returning to the beginning of the line.
A CR immediately followed by a LF (CRLF,
\r\n
, or0x0D0A
) moves the cursor to the beginning of the line and then down to the next line.๐กThe first\r\n
signifies the end of the status line.๐กThe second\r\n
signifies the end of the response headers section (which is empty in this case).
Now that We have completed our second step, Let's move forward to Step 3
Step 3 : Respond with 404
To be honest, this step took me a while to pass all the test cases.
What are the tasks?
- In this stage, your program will need to extract the path from the HTTP request.
Now, on the site it was given about how a HTTP request looks like:
GET /index.html HTTP/1.1 Host: localhost:4221 User-Agent: curl/7.64.1
and all of these lines are separated by
\r\n
.First we will convert the above req into string and then split the above request in an array(because we need only Line 1 i.e.
GET /index.html HTTP/1.1
we will use JS method to do it, and will store it in a variable:
const request = data.toString().split("\r\n"); // The Above request will look like [ "GET /index.html HTTP/1.1", "Host: localhost:4221", "User-Agent: curl/7.64.1" ]
now we need only line 1, so we will use another JS method and store it in a variable:
const path = request[0].split(" ")[1]; // This will give us - /index.html
And this is what we need, the path.
Now we will check if the path is
/
or not and give the response accordinglyif (path === "/") { socket.write("HTTP/1.1 200 OK\r\n\r\n"); } else { socket.write("HTTP/1.1 404 Not Found\r\n\r\n"); }
The error I was getting and I got lost into that, I wasn't ending the current socket connection, so in the last of server we need to specify
socket.end()
.At this moment, everything was correct, but, I was mistaking "data", which I was giving as the buffer to the server.on("data", callback). I need to pass the data as parameter in the callback function.
Now the complete code looks like this:
```javascript const { error } = require("console"); const net = require("net");
// You can use print statements as follows for debugging, they'll be visible when running tests. console.log("Logs from your program will appear here!");
// Uncomment this to pass the first stage const server = net.createServer((socket) => { socket.on("close", () => { socket.end(); server.close(); }); socket.on("error", console.error); socket.on("data" , (data) => {
const request = data.toString().split("\r\n"); const path = request[0].split(" ")[1];
if (path === "/") { socket.write("HTTP/1.1 200 OK\r\n\r\n"); } else { socket.write("HTTP/1.1 404 Not Found\r\n\r\n"); } socket.end(); })
});
server.listen(4221, "localhost");
## Step 4: **Respond with content**
In this step, your program will need to respond with a body. In the previous stages we were only sending a status code, no body.
The task here is to parse the path from the HTTP request. We will send a random string in the url path you will need to parse that string and then respond with the parsed string (only) in the response body.
The tester will send you a request of the form `GET /echo/<a-random-string>`.
Your program will need to respond with a 200 OK response. The response should have a content type of `text/plain`, and it should contain the random string as the body.
As an example, here's a request you might receive:
```javascript
GET /echo/abc HTTP/1.1
Host: localhost:4221
User-Agent: curl/7.64.1
And here's the response you're expected to send back:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 3
abc
So from Step 3, we had the complete path.
Now we will split that path using split()
method, and we will split it
const stringpassed = path.split("/echo/")[1]
// It splits the path variable using the string "/echo/" as a separator.
// It then tries to grab the second element (index 1) of the resulting array.
// We will get the string that is after "/echo/{string}"
After this we will send the response that was expected, but before sending we will check if the "/echo/" exist or not.
else if (path.startsWith("/echo/")) {
let ans = "";
ans += "HTTP/1.1 200 OK\r\n";
ans += "Content-Type:text/plain\r\n";
ans += `content-Length:${stringpassed.length}\r\n\r\n`;
ans += stringpassed;
socket.write(ans);
}
And now our Step 4 is done and dusted.
Step 5: Parse Headers
This is somehow similar to Step 4, we need to create a new variable to store the header value
const header = request[2].split("User-Agent: ")[1];
and then pass the
else if(path === "/user-agent"){
let ans = "";
ans += "HTTP/1.1 200 OK\r\n";
ans += "Content-Type:text/plain\r\n";
ans += `content-Length:${header.length}\r\n\r\n`;
ans += header;
socket.write(ans);
}
and that's it, we are done with Step-5.
Up until now the complete code looks like
const { error } = require("console");
const net = require("net");
// You can use print statements as follows for debugging, they'll be visible when running tests.
console.log("Logs from your program will appear here!");
// Uncomment this to pass the first stage
const server = net.createServer((socket) => {
socket.on("close", () => {
socket.end();
server.close();
});
socket.on("error", console.error);
socket.on("data" , (data) => {
const request = data.toString().split("\r\n");
const path = request[0].split(" ")[1];
const header = request[2].split("User-Agent: ")[1];
const stringpassed = path.split("/echo/")[1]
if (path === "/") {
socket.write("HTTP/1.1 200 OK\r\n\r\n");
}
else if (path.startsWith("/echo/")) {
let ans = "";
ans += "HTTP/1.1 200 OK\r\n";
ans += "Content-Type:text/plain\r\n";
ans += `content-Length:${stringpassed.length}\r\n\r\n`;
ans += stringpassed;
socket.write(ans);
}
else if(path === "/user-agent"){
let ans = "";
ans += "HTTP/1.1 200 OK\r\n";
ans += "Content-Type:text/plain\r\n";
ans += `content-Length:${header.length}\r\n\r\n`;
ans += header;
socket.write(ans);
}
else {
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
}
socket.end();
})
});
server.listen(4221, "localhost");
Step-6: Concurrent connections
So this step was very easy, till now we were closing after calling server.close()
, because server.close()
to stops the server from accepting new connections.
So remove it from the code.
socket.on("close", () => {
socket.end();
server.close();
});
// it should be now
socket.on("close", () => {
socket.end();
// no socket.close()
});
Step-7 : Get A File and Post a File
Sure, here's the final part of the blog explaining the problem statement and the code implementation:
Problem Statement Revisited
In the previous sections, we discussed the requirements for implementing a simple file server that can handle GET requests for retrieving files and POST requests for uploading files. Specifically, the file server program needs to accept a --directory
flag when executed, which specifies the directory where files should be served from or uploaded to.
For GET requests of the format GET /files/<filename>
, the server should respond with the contents of the requested file if it exists in the specified directory. The response should have a 200 OK
status code, the Content-Type
header set to application/octet-stream
, and the file contents as the response body. If the requested file doesn't exist, the server should respond with a 404 Not Found
status code.
For POST requests to the /files
route, the server should handle file uploads. The uploaded file should be saved in the specified directory, and the server should respond with a 201 Created
status code upon successful upload.
Code Implementation
Here's the final code implementation that addresses the problem statement:
const { error } = require("console");
const net = require("net");
const fs = require("fs");
const path = require("path");
const filePath = process.argv[3];
const server = net.createServer((socket) => {
socket.on("close", () => {
socket.end();
});
socket.on("error", console.error);
socket.on("data", async (data) => {
const request = data.toString().split("\r\n");
const pathRequest = request[0].split(" ");
const path = request[0].split(" ")[1];
const header = request[2].split("User-Agent: ")[1];
const stringpassed = path.split("/echo/")[1];
if (path === "/") {
socket.write("HTTP/1.1 200 OK\r\n\r\n");
} else if (path.startsWith("/echo/")) {
let ans = "";
ans += "HTTP/1.1 200 OK\r\n";
ans += "Content-Type:text/plain\r\n";
ans += `content-Length:${stringpassed.length}\r\n\r\n`;
ans += stringpassed;
socket.write(ans);
} else if (path === "/user-agent") {
let ans = "";
ans += "HTTP/1.1 200 OK\r\n";
ans += "Content-Type:text/plain\r\n";
ans += `content-Length:${header.length}\r\n\r\n`;
ans += header;
socket.write(ans);
} else if (pathRequest[1].includes("/files/")) {
if (request[0].includes("POST")) {
const fileName = pathRequest[1].replace("/files/", "");
const file = path.join(filePath, fileName);
const content = request[request.length - 1];
await fs.promises.writeFile(file, content);
socket.write("HTTP/1.1 201 Created\r\n\r\n");
}
const fileName = pathRequest[1].replace("/files/", "");
const file = path.join(filePath, fileName);
if (fs.existsSync(file)) {
const content = await fs.promises.readFile(file);
socket.write(
`HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: ${content.length}\r\n\r\n${content}`
);
} else {
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
socket.end();
}
} else {
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
}
socket.end();
});
});
server.listen(4221, "localhost");
But what is written in the above code?
We import the necessary modules:
console
for logging errors,net
for creating the server,fs
for file system operations, andpath
for working with file paths.We get the directory path from the command-line arguments (
process.argv[3]
).We create a new TCP server using
net.createServer
.In the server callback function, we handle various events:
close
: We end the socket when the client closes the connection.error
: We log any errors that occur.data
: We handle incoming data from the client.
For incoming data, we parse the request and extract the requested path, header, and any string passed in the request body.
We check the requested path and handle different cases:
/
: We respond with a200 OK
status code and an empty body./echo/...
: We respond with a200 OK
status code, theContent-Type
set totext/plain
, theContent-Length
set to the length of the string passed in the request body, and the string itself as the response body./user-agent
: We respond with a200 OK
status code, theContent-Type
set totext/plain
, theContent-Length
set to the length of the user agent string, and the user agent string as the response body./files/...
:If it's a POST request, we extract the file name from the path, construct the file path by joining the provided directory and the file name, and write the request body to the file using
fs.promises.writeFile
. We respond with a201 Created
status code upon successful file upload.If it's a GET request, we extract the file name from the path, construct the file path by joining the provided directory and the file name, and check if the file exists using
fs.existsSync
. If the file exists, we read its contents usingfs.promises.readFile
and respond with a200 OK
status code, theContent-Type
set toapplication/octet-stream
, theContent-Length
set to the length of the file contents, and the file contents as the response body. If the file doesn't exist, we respond with a404 Not Found
status code.
For any other path, we respond with a
404 Not Found
status code.
After sending the response, we end the socket connection.
Finally, we start the server and listen on port
4221
andlocalhost
.
By following this code and the explanations provided throughout the blog, you should be able to create a fully functional file server that meets the specified requirements. Do checkout the Challenge by CodeCrafters
Here's the complete code on github:
Subscribe to my newsletter
Read articles from Ankit Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ankit Mishra
Ankit Mishra
๐ค Exploring new technologies and developing software solutions and quick hacks. ๐ Studying Electronics and Communication Engineering at Chandigarh Engineering College. ๐ฑ Learning more about Full stack web development, Data Structure and Algorithm. โ๏ธ Pursuing Blog Writing as hobby.