Table of Contents

How to Build a Web App with JavaScript

Finally, it is time for us to create our first real web application using JavaScript.

Initialize a new Node.js project

Let's first a new work directory with the following command:

bash
mkdir <work_directory>

And then change into it:

bash
cd <work_directory>

Under your work directory, initialize a new Node.js project:

bash
npm init -y

New Project

A package.json file should be generated:

text
<work_directory>
└── package.json

And finally, create a new server.js file and a data.txt file. server.js will contain the source code, and data.txt will act as our database.

text
<work_directory>
├── data.txt
├── package.json
└── server.js

The file system

There are two packages we must discuss to make our first web app.

Let's start with the file system (fs) package. It is built into Node.js, so you don't need to install anything.

This is how we can read the content of data.txt:

server.js

javascript
import { readFile } from "fs";
 
// Specify the file we want to read as well as the charset encoding format
readFile("data.txt", "utf8", (error, text) => {
  if (error) throw error;
 
  console.log(text); // Log the file content to the console
});

We can also write to data.txt like this:

server.js

javascript
import { writeFile } from "fs";
 
writeFile("data.txt", "Hello, World? Hello, World!", (error) => {
  if (error) {
    console.log(`${error}`);
  } else {
    console.log("File written.");
  }
});

In this case, it is not necessary to specify the encoding format. If writeFile is given a string, it will simply assume the default format, UTF-8.

The HTTP module

Another very important module we need to talk about is http. It allows us to create an HTTP server. For example:

server.js

javascript
import { createServer } from "http";
 
let server = createServer((request, response) => {
  response.writeHead(200, { "Content-Type": "text/html" });
 
  response.write(`<h1>Hello, World!</h1>`);
 
  response.end();
});
 
server.listen(8000); // Configure the HTTP server to listen on port 8000
console.log("Listening! (port 8000)");

The variables request and response each represent an object storing the incoming and outgoing data. For instance, you can access the url property of the request by using request.url.

javascript
let server = createServer((request, response) => {
  const url = request.url;
 
  // . . .
});

Start this server by running the following command:

bash
node server.js

Open your browser and go to http://localhost:8000/. You should see a Hello, World! message.

This example is fairly basic, in reality, the backend servers are usually more complex. So next, let's try something more challenging.

We are going to create a web app that asks for your name, and once you submit your name, it will be stored in a txt file, which acts as a database.

When you visit the web page again, it will greet you with your name.

Creating a web app

To get started, our server.js needs some updates. We need to consider two different scenarios:

  • When you visit http://localhost:8000/, the browser will send a GET request to the backend requesting an HTML page, which contains a web form.
  • When you submit the form, that will send a POST request to the backend.

Under the request object, there is a method property that tells you what request method is used. Our server must deal with GET and POST requests differently.

server.js

javascript
import { createServer } from "http";
import { writeFile, readFile } from "fs";
 
let server = createServer((request, response) => {
  if (request.method === "GET") {
    // . . .
  } else if (request.method === "POST") {
    // . . .
  }
});
 
server.listen(8000);
console.log("Listening! (port 8000)");

Dealing with the GET request

When dealing with the GET request, you need to read the name from the data.txt file first.

server.js

javascript
// . . .
if (request.method === "GET") {
  // Read data.txt and get name
  readFile("data.txt", "utf8", (error, name) => {
    if (error) throw error;
 
    response.writeHead(200, { "Content-Type": "text/html" });
    // Embed name into the response
    response.write(`
      <h2>Hello, ${name}</h2>
      <p>What is your name?</p>
      <form method="POST" action="/">
        <p>Name: <input type="text" name="name"></p>
        <p><button type="submit">Submit</button></p>
      </form>
    `);
    response.end();
  });
} else if (request.method === "POST") {
  // . . .
}
// . . .

An error will be thrown if something goes wrong while reading the file.

If not, a 200 OK response will be returned. The read data will be assigned to the variable name and be used to print the webpage.

Dealing with the POST request

As for the POST request:

server.js

javascript
// . . .
if (request.method === "GET") {
  // . . .
} else if (request.method === "POST") {
  let data;
 
  request.on("data", function (chunk) {
    data = chunk.toString();
  });
 
  request.on("end", function () {
    let name = data.split("=")[1];
 
    writeFile("data.txt", name, function (error) {
      if (error) throw error;
 
      response.writeHead(302, { Location: "/" });
      response.end();
    });
  });
}
// . . .

This is when the backend receives a request to add new data, and we need to consider two different cases:

While the data is being transferred (request.on("data", . . .)), notice that there is a chunk variable inside the callback function.

server.js

javascript
// . . .
if (request.method === "GET") {
  // . . .
} else if (request.method === "POST") {
  let data;
 
  request.on("data", function (chunk) {
    data = chunk.toString();
  });
 
  request.on("end", function () {
    let name = data.split("=")[1];
 
    writeFile("data.txt", name, function (error) {
      if (error) throw error;
 
      response.writeHead(302, { Location: "/" });
      response.end();
    });
  });
}
// . . .

This variable contains the actual data that is transferred, which is a Buffer. We will discuss more about it later. For now, you only need to know that you can convert it into a string using the toString() method, which you should already be familiar with.

The converted string is then assigned to the variable data, which has a higher scope, so it can be accessed later.

server.js

javascript
// . . .
if (request.method === "GET") {
  // . . .
} else if (request.method === "POST") {
  let data;
 
  request.on("data", function (chunk) {
    data = chunk.toString();
  });
 
  request.on("end", function () {
    let name = data.split("=")[1];
 
    writeFile("data.txt", name, function (error) {
      if (error) throw error;
 
      response.writeHead(302, { Location: "/" });
      response.end();
    });
  });
}
// . . .

After the data has been transferred and received (request.on("end", . . .)), we get the name from the variable data.

server.js

javascript
// . . .
if (request.method === "GET") {
  // . . .
} else if (request.method === "POST") {
  let data;
 
  request.on("data", function (chunk) {
    data = chunk.toString();
  });
 
  request.on("end", function () {
    let name = data.split("=")[1];
 
    writeFile("data.txt", name, function (error) {
      if (error) throw error;
 
      response.writeHead(302, { Location: "/" });
      response.end();
    });
  });
}
// . . .

Recall that data has a key/value format, key=value.

And the split() method splits the string according to the given character, and the result will be returned as an array.

Putting them together, data.split("=") splits key=value into [key, value], and [1] accesses the second item in the array, which is value.

javascript
request.on("end", function () {
  let name = data.split("=")[1];
 
  writeFile("data.txt", name, function (error) {
    if (error) throw error;
 
    response.writeHead(302, { Location: "/" });
    response.end();
  });
});

Finally, the name will be stored in the data.txt file using writeFile().

Start the server with the following command:

bash
node server.js

Open the browser, go to http://localhost:8000/, and submit a new name. When you refresh the page or restart the server, your name should persist.