Create a Web Sever with Nodejs HTTP Module

Oct 26, 2022 Create a Web Sever with Nodejs HTTP Module

Have you ever wondered if you could create a web server without any help from third-party libraries like ExpressJS, NestJS, etc?

I remember my first NodeJS web server was developed with ExpressJS. Didn’t know what type of headers we are sending, how the HTTP request methods and routes are working, and all these deep kinds of stuff.

In my opinion, Nodejs built-in HTTP module is the perfect one to learn all of these. Let’s get started.

If you want to access the final result before starting check out this GitHub repository.

Basic HTTP Server

First, create a directory. I am going to name the folder nodejs-http-rest-api so that I can recognize the repo easily on GitHub. Open the directory on the terminal to run npm init -y. It will create a package.json file for you with default options.

To make things easier we are going to install the nodemon package to restart the server whenever we make changes to the code.

npm i -D nodemon

Now we have to add the "type": "module" to enable ECMAScript and add the dev npm script in package.json like the image below.

Nodejs nodemon dev script in package.json

Create a file called index.js and paste the code below.

import http from "node:http";

const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'application/json'})
    res.end(JSON.stringify({
        data: 'Hello World!'
    }))
})

server.listen(8000, () => {
    console.log('Server is running on http://localhost:8000');
})

So, what we are doing here is:

  • We imported the Nodejs HTTP module, and we used the http.createServer method to create the server.
  • Inside the createServer function, we set the response status code and header of the server response with the res.writeHead method. Since we are sending a JSON response, we set the content type to JSON. and then we send the JSON data with the res.end function. You might be wondering why we use JSON.stringify() when we are sending a JSON response. It’s because response.end() only accepts a string or Buffer as an argument. Also, the arguments req and res are the short forms of request and response. These arguments are responsible for the request and response of the server.
  • And lastly, server.listen() starts the server listening for connections.

Now, save the file and run npm run dev on the terminal.

You should see the Server is running on http://localhost:8000 message on your terminal.

Open postman or any API testing app and send a request to http://localhost:8000. You will see the JSON response with a 200 status code like below.

Nodejs basic http server response postman

Routing

Routing means having support for multiple endpoints in your API. For example, for all users on the server, the endpoint should be /users and for all to-dos, the endpoint should be /todos. Let’s add a /todos endpoint to send all to-dos from the server.

import http from "node:http";

const todos = [
  {
    id: 1,
    title: "Check Emails",
  },
  {
    id: 2,
    title: "Buy Rice",
  },
];

const server = http.createServer((req, res) => {
  switch (req.url) {
    case "/todos":
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(todos));
      break;
    case "/":
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify({
          message: "Hello World!",
        })
      );
  }
});

server.listen(8000, () => {
  console.log("Server is running on http://localhost:8000");
});

Here is what is changed:

  • First, we created a demo todos array to send them from the/todos endpoint.
  • To get the endpoint, we used req.url which returns the URL.
  • Inside the switch statement, if the URL matches any of these two endpoints, we are sending data with 200 responses.

To practice more you can add /users endpoint like to-dos.

Serving a Web Page

Till now, we have only worked on the API. why not have an HTML web page too?

Let’s create a file named index.html in the project folder and add below HTML content in the file.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Server with Nodejs HTTP Module</title>
  </head>
  <body>
    <h1>Hi There! 👋</h1>
    <p>This page was sent from Nodejs Server</p>
  </body>
</html>

Now, we are going to use Nodejs fs/promises to read the index.html file and send the file content from the / endpoint.

import http from "node:http";
import { readFile } from "node:fs/promises";

const todos = [
  {
    id: 1,
    title: "Check Emails",
  },
  {
    id: 2,
    title: "Buy Rice",
  },
];

const server = http.createServer((req, res) => {
  switch (req.url) {
    case "/todos":
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(todos));
      break;
    case "/":
      const filePath = new URL("./index.html", import.meta.url);
      readFile(filePath, { encoding: "utf8" }).then((htmlContent) => {
        res.writeHead(200, { "Content-Type": "text/html" });
        res.end(htmlContent);
      });
  }
});

server.listen(8000, () => {
  console.log("Server is running on http://localhost:8000");
});

What we are doing here is:

  • We imported readFile from the fs/promises module to read the HTML file.
  • On the / endpoint, we get the index.html file path by creating a new URL. The import.meta.url property returns the project location.
  • Then we read the file using readFile and uft8 encoding. With uft8 encoding, we get the file content in a text format instead of the raw buffer.
  • After successfully getting the file content we send the file content. Notice we set the header content type as text/html since we are sending HTML content.

Now visit http://localhost:8000 from the browser to view the HTML page as below.

Served html page from nodejs http

HTTP Request Methods

If we want to add more or delete to-dos, we have to use HTTP request methods like POST, DELETE, etc. Till now what we have been using is the GET method. Let’s update the /todos endpoint to enable request methods.

import http from "node:http";
import { readFile } from "node:fs/promises";
import { Buffer } from "node:buffer";

const todos = [
  {
    id: 1,
    title: "Check Emails",
  },
  {
    id: 2,
    title: "Buy Rice",
  },
];

const sendTodosReposne = (res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify(todos));
};

const server = http.createServer((req, res) => {
  switch (req.url) {
    case "/todos":
      if (req.method === "GET") {
        sendTodosReposne(res);
      } else {
        let reqBody = [];
        req
          .on("data", (chunk) => {
            reqBody.push(chunk);
          })
          .on("end", () => {
            reqBody = JSON.parse(Buffer.concat(reqBody).toString());
            if (req.method === "POST") {
              todos.push({
                id: todos[todos.length - 1].id + 1,
                title: reqBody.title,
              });
              sendTodosReposne(res);
            } else if (req.method === "DELETE") {
              const indexOfTodo = todos.findIndex(
                (todo) => todo.id === reqBody.id
              );
              todos.splice(indexOfTodo, 1);
              sendTodosReposne(res);
            }
          });
      }
      break;
    case "/":
      const filePath = new URL("./index.html", import.meta.url);
      readFile(filePath, { encoding: "utf8" }).then((htmlContent) => {
        res.writeHead(200, { "Content-Type": "text/html" });
        res.end(htmlContent);
      });
  }
});

server.listen(8000, () => {
  console.log("Server is running on http://localhost:8000");
});

What happened here is:

  • We moved the /todos default response to a reusable function so that we can use this function for every request method.
  • Then we are using the If statement based on the req.method which tells us the method of request. For the GET request, we are sending the data as before.
  • But, for PUT and DELETE methods, we are getting the request body data first with the request.on listener and data event. When getting requests, we are storing chunks of body data in the reqBody array and with the end event, we confirm that all data is received. Then convert the Buffer to text and then JSON format. You can read more info about the request body here.
  • After getting the request body, we modify the to-dos array based on the request method. if the method is PUT, we add a new to-do value with the title value of the request body. Notice that, we are increasing the id value to keep the id value unique. Of course, In a real-world application, you have to use UUID. But for this demo purpose, it gets the job done. And for the DELETE method, we just delete the to-do from the to-dos array by finding the index. of course, when you restart the server, it will go back to the original state because we are not storing this data anywhere.

With this approach, you can also implement other HTTP request methods like PUT, PATCH, etc. For the DELETE methods or getting single to-do, there is another way to accomplish this which is to use search params in API URL. for example, http://localhost:8000/todos?id=1. For simplicity, I used this approach. Also, my main goal is to explain how the HTTP module works. Let me know if you also want to learn this approach too.

All right let’s test the API one last time.

Post request nodejs http module Delete request nodejs http module

As you can see, with the title value and POST request the to-dos got updated, and with the id value and DELETE request the to-do with id value 1 got deleted.

I could use the body-parser package to ignore all these body-parsing complications. But I wanted to show this thinking you might learn something new.

Conclusion

So, there you have it. Now you know how the HTTP module works. You may think that you will never use the HTTP module because of this complexity. But you will end up using HTTP and HTTPS modules. especially when it comes to Nodejs stream.