DEV Community

Cover image for Beginner's guide to creating a Node.js server
Lisa
Lisa

Posted on • Updated on

Beginner's guide to creating a Node.js server

As full stack developers, we use multiple programming languages to build the frontend and backend of our apps. I often find myself mixing the syntax of JavaScript and Ruby as I switch back and forth between frontend and backend.

What is a programmer to do to keep everything straight?!

Node.js solves this exact pain point. It allows JavaScript developers to write both the client-side and server-side code without having to learn a completely different language.

But what exactly is Node.js? When you look up Node.js, you will see it defined as a JavaScript runtime built on Chrome's V8 JavaScript engine(definition from Node.js).

alt text image

If you only recognized the words JavaScript, Chrome, and engine from that definition and still cannot grasp what Node.js is, you are not alone. I was on the same boat and thought it is about time I find out what Node.js is all about.

So let's get to it!

By the end of this blog, you will be able to:

  1. define Node.js in your own words
  2. learn enough basics to create a Node.js server and create routes that handle different http requests.

What is Node.js?

Node.js is a free, open-sourced, cross-platform JavaScript run-time environment that lets developers write command line tools and server-side scripts outside of a browser(excerpt from Node.js).

Don't worry if this definition does not make sense yet. These concepts will be explained in detail in the following sections.

History of Node.js

JavaScript is a programming language originally developed to run only in the browser. It manipulates the DOM and adds interactivity to your website.

Javascript is executed by Javascript engines. Basically, this engine takes JavaScript code and compiles it into machine code that computers can work with more efficiently. There are multiple Javascript engines available. However, Node.js selected the V8 engine developed by Google to run Javascript.

As JavaScript grew more in popularity, major browsers competed to offer users the best performance. More development teams were working hard to offer better support for JavaScript and find ways to make JavaScript run faster. Around that time, Node.js was built on V8 JavaScript engine(excerpt from Node.js) and gained popularity among developers for the following reasons.

Defining characteristics of Node.js

Characteristic #1 With Node.js, you can write server-side code with JavaScript

Like JavaScript, Node.js runs on V8 JavaScript engine. The creators of Node.js took the V8 code base and have added multiple features to it. These features have made it possible for Node.js users to build servers with JavaScript.

With Node.js, you can now build a server that connects to the database to fetch and store data, authenticates user, validates input and handles business logic.

Characteristic #2 Node.js is not limited to the server. You can use Node.js for utility scripts or for building tools.

While Node.js is most commonly used for web development and server-side code, you can do other things with it! Because Node.js is a JavaScript runtime, you can execute any JavaScript code with Node.js.

For example, Node.js has the ability to access the file system so it can read, write and manipulate files. This feature allows you to use Node.js to handle a lot of utility tasks on your computer without exposing files to the public.

Characteristic #3 Node.js uses an event driven code for running your logic. Because of that, JavaScript thread is always free to handle new events and new incoming requests.

Node.js involves a lot of asynchronous code, meaning that it registers callbacks and events to be executed in the future instead of being executed right away. This characteristic is what allows Node.js to run in a non-blocking fashion and it is what makes Node.js apps to be very performant.

Now that we have covered the basic concepts, let's get our hands dirty and build a server with Node.js!

Creating a server and routes with Node.js

This is what we will be building!
Node blog

We will be creating a very simple server that can handle requests from a browser.

On the browser side, the user will be greeted with a welcome message and will be asked to submit their mood through a form.

The server will receive the user input and it will create a file to store user input.

We will be accomplishing all of these tasks without the help of frameworks like Express. This may be a harder way to learn Node.js but it will help us understand how Node.js actually works under the hood!

After mastering the concepts in this blog, check out my next blog on how to create a Node.js server using Express as a framework. It will give you a greater appreciation for Express as it will accomplish a lot of the work we will be doing in this blog with fewer lines of code!

Prerequisite Download
Download Node.js here. Save it and run the installer.

The code for the server is included in this GitHub repo. Feel free to refer to it if you encounter a bug while you follow along!

Step 1: Create a directory for our server
In the appropriate directory, type the following in your terminal to create a directory for our server.

mkdir All_The_Feels
Enter fullscreen mode Exit fullscreen mode

Get into All_The_Feels directory and open it up in your text editor.

cd All_The_Feels
code .
Enter fullscreen mode Exit fullscreen mode

Step 2: Create server.js and routes.js files within All_The_Feels directory
In your terminal, execute the following command.

touch server.js routes.js
Enter fullscreen mode Exit fullscreen mode

You will see that server.js and routes.js files have been created within your directory.
Alt Text

In the server.js file, we will we will import all the necessary components to set up a server. Server will be set up to listen for client requests.

In the routes.js file, we will build routes to handle various client requests and send an appropriate response to the browser. We will also be writing code here to save user input in a separate file in our server.

We will first focus on server.js. The final version of server.js has been provided for you in the image below. Steps 3-5 will include corresponding lines of code specified in the image so you can easily follow along!
Alt Text

Step 3: Import http module in server.js
There are several core modules available in Node.js. Among these, http core module has the ability to launch a server.

To use the features of http module, we need to import it into server.js by using the require() keyword. In server.js, create a http constant and require http as shown below.

#In server.js(line 1)

const http = require('http')
Enter fullscreen mode Exit fullscreen mode

Now we can use the features of http module!

Step 4: Import routes into server.js and create a server
One of the functionalities of http module is the createServer() method. This method creates a server and accepts a requestListener function that has two parameters: HTTP request(req) and response(res).

However, we will be passing routes here instead as we will be defining requestListener in routes.js. But more on that later!

Create a server by declaring server as a constant and setting it equal to createServer method and passing routes as its argument.

#In server.js(line 5)

const server = http.createServer(routes)
Enter fullscreen mode Exit fullscreen mode

In order for us to pass routes as an argument, we need to import routes.js into server.js. To do this, declare routes as a constant and require routes by providing the file path.

#In server.js(line 3)

const routes = require("./routes")
Enter fullscreen mode Exit fullscreen mode

Lastly, our server needs to listen for incoming requests from the browser. We accomplish that by using the listen() method to create a listener on a specified port. Pass in 3000 as an argument in server.listen() method.

#In server.js(line 7)
server.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Now that we have configured server.js to create a server, let's focus on routes.js. Our goal is to create a requetListener function that takes in client request and server response as arguments. We will build routes to handle various client requests and send an appropriate response to the browser.

The final version of routes.js has been provided for you below to avoid any confusion as you follow along. The following steps will discuss the code line by line!

#in routes.js

const fs = require("fs");

const requestListener = (req, res) => {
  const url = req.url;
  const method = req.method;
  if (url === "/") {
    res.write("<html>");
    res.write("<head><title>All the Feels</title></head>");
    res.write(
      '<body><h1>Hey there, welcome to the mood tracker!</h1><p>Enter your mood below and hit send to save your mood.</p><form action = "/mood" method="POST"><input type = "text" name="mood"><button type="submit">Send</button></body>'
    );
    res.write("</html>");
    return res.end();
  }
  if (url === "/mood" && method === "POST") {
    const body = [];
    req.on("data", (chunk) => {
      body.push(chunk);
    });
    return req.on("end", () => {
      const parsedBody = Buffer.concat(body).toString();
      console.log(parsedBody)
      const mood = parsedBody.split("=")[1];
      fs.writeFile("user_mood.txt", mood, () => {});
      return res.end();
    });
  }
};

module.exports = requestListener;
Enter fullscreen mode Exit fullscreen mode

Step 5: Create a requestListener in routes.js and export routes
In routes.js, copy and paste the following.

# in routes.js

const requestListener = (req, res) => {
  console.log(req)
};
module.exports = requestListener;
Enter fullscreen mode Exit fullscreen mode

Let's break this down!

We will start with the last line of code:

module.exports = requestListener;
Enter fullscreen mode Exit fullscreen mode

Earlier in step #4, I mentioned that createServer() method in server.js accepts a requestListener function.

#In server.js(line 5)

const server = http.createServer(routes)
Enter fullscreen mode Exit fullscreen mode

However, we passed routes as an argument instead as we are defining requestListener in routes.js.

We need to export routes file so that routes could be imported into server.js. We do that by using the module.exports keyword.

module.exports = requestListener;
Enter fullscreen mode Exit fullscreen mode

Let's get back to the top of the code!

requestListener is a function that is executed whenever the server receives an incoming request. This function takes in two arguments:

  1. request: incoming
  2. response: serverResponse
# in routes.js

const requestListener = (req, res) => {
  console.log(req)
};
module.exports = requestListener;
Enter fullscreen mode Exit fullscreen mode

Request and response are abbreviated as req and res as shown in the code sample above. Both request and response are objects that contain a lot of information about the request(req) sent from the browser and the response(res) that server sends to the browser.

In the current code, I have included console.log(req) here to show you what a typical request from a browser looks like. To view the req, fire up the server by running the following command in the terminal.

#in terminal

node server.js
Enter fullscreen mode Exit fullscreen mode

Open up a chrome browser and type in localhost:3000 in the url bar. Nothing should be showing on the page at the moment. Go back to your text editor.

In the terminal, you will see a req object that includes a ton of information as key value pairs.
Alt Text

For the purpose of this tutorial, we will be focusing on keys- url, method, and headers in the request. To view what these look like, replace the code in routes.js with the following.

#in routes.js

const requestListener = (req, res) => {
  console.log(req.url, req.method, req.headers)
};

module.exports = requestListener;
Enter fullscreen mode Exit fullscreen mode

With the current set up we have, we have to manually restart our server every time we want to see the results after making changes to our code. There are tools that does this for you but for the purpose of this blog, we will exit the server by hitting control + c on your keyboard and restart the server by typing node server.js in your terminal.

Refresh your browser and go back to your text editor.

You will see the following in your terminal.
Alt Text

The url in the request object is highlighted with a red box. "/" denotes that localhost:3000 is making the request. If the url of the browser was "localhost:3000/moods", "/moods" should be displayed as the url in the request object.

The method of the request is highlighted with a blue box. Since we have not specified method on the browser side, it is going to send a default GET request to our server.

The {} contains the header. It includes information about the host, which browser we used for that request, and what type of request we would accept and etc.

Step 6: Configure a "/" route to display a greeting message and a form that takes user input
Our browser(localhost:3000) is sending a GET request to our server but the browser is not displaying anything because our server is not sending back a response. As we will not be writing front end code in this tutorial, we will send some html code as a response to display in the browser.

If a user is sending a request from a localhost:3000 url, we will send html code that displays a greeting message and a form where the user can submit their mood. We will accomplish this by replacing the code in routes.js with the following code.

# in routes.js

const requestListener = (req, res) => {
  const url = req.url;
  if (url === "/") {
    res.setHeader("Content-Type", 'text/html')
    res.write("<html>");
    res.write("<head><title>All the Feels</title></head>");
    res.write(
      '<body><h1>Hey there, welcome to the mood tracker!</h1><p>Enter your mood below and hit send to save your mood.</p><form action = "/mood" method="POST"><input type = "text" name="mood"><button type = "submit">Send</button></body>'
    );
    res.write("</html>");
    return res.end();
  }
};
module.exports = requestListener;
Enter fullscreen mode Exit fullscreen mode

Let's go over this line by line!

As the url of the request will determine what response we will send to the client, we need to first grab the url from the req object.

Create a constant called url and set it equal to url in req object.

# in routes.js

const url = req.url;
Enter fullscreen mode Exit fullscreen mode

If the value of url is "/"(meaning localhost:3000), we will send the following html code as a response.

# in routes.js

  if (url === "/") {
    res.setHeader("Content-Type", 'text/html')
    res.write("<html>");
    res.write("<head><title>All the Feels</title></head>");
    res.write(
      '<body><h1>Hey there, welcome to the mood tracker!</h1><p>Enter your mood below and hit submit to save your mood.</p><form action = "/mood" method="POST"><input type = "text" name="mood"><button type = "submit">Send</button></body>'
    );
    res.write("</html>");
    return res.end();
  }
Enter fullscreen mode Exit fullscreen mode

res.setHeader() is a method that creates a header for our response. The header lets the browser know what type of content is in our response object. Since we are sending html code, we set our Content-Type to be text/html.

res.write() is a method that allow us to write the data we are going to send in a response. In Node.js, you can write html code exactly like how you would in the frontend. However, you must start every line with res.write and include the html code in parenthesis as shown above.

As you can see, we declare that we are writing html code and set the title of our browser tab to be "All the Feels".

The body tag contains multiple elements so let's break it down.

  • h1 tag contains a greeting message(Hey there, welcome to the mood tracker!)
  • p tag contains directions for the user(Enter your mood below and hit submit to save your mood.)
  • form tag contains action and method attributes. Action attribute specifies where to send the form-data when a form is submitted. We have specified the location to be /mood. Method specifies that we are sending a POST request to the server upon form submission.
  • input tag states that the type of user input will be text and input name is mood. -button tag creates a button labeled as "Send" and once it's clicked, it will send the request.

We write res.end() to signify we are finished writing the data in our response.

All right! Let's restart the server by exiting out of the server(control + C) and firing up the server(node server.js).

Go to your browser(localhost:3000), you will see the response displayed on our page!
Alt Text

Open up DevTools by pressing control + Shift + J on your keyboard. Click on the network tab and refresh your browser. Click on localhost under the name column(red arrow).
Alt Text

You will see that our get request got a status code of 200, meaning that the get request succeeded at getting appropriate data from the server(green box).

If you look at response headers(orange box), you will also see the response header we have specified in our response.
Alt Text
Click on response tab(red box). You will see the content of our response we have written in our server!

So far, we have been able to successfully create a route for get request and send our response to the browser. Next step is to save user's input in a separate file in our server!

Step 7: Save user's input in a separate file
Before we delve into the code, we need to get familiar with how Node.js handles data, a concept also known as streams.

Instead of waiting for the entire incoming data to be read into memory, Node.js reads chunks of data piece by piece, processing its content without keeping it all in memory(excerpt from NodeSource).

The chunks of data are further grouped into buffers. Your code can now recognize these buffers and start working with the data.

This is extremely powerful when working large amounts of data(ex.streaming videos) and it increases memory and time efficiency of your app!

Even though our user input is very small, our code will reflect how Node.js processes data.

All right, let's get to the code!

Copy and paste the following code after the previous if statement we have written.

# in routes.js

 if (url === "/mood" && method === "POST") {
    const body = [];
    req.on("data", (chunk) => {
      body.push(chunk);
    });
    return req.on("end", () => {
      const parsedBody = Buffer.concat(body).toString();
      const mood = parsedBody.split("=")[1];
      fs.writeFile("user_mood.txt", mood, () => {});
      return res.end();
    });
  }
Enter fullscreen mode Exit fullscreen mode

Remember our html code for form.

# in routes.js

<form action = "/mood" method="POST"><input type = "text" name="mood">
Enter fullscreen mode Exit fullscreen mode

When a user submits the form, /mood url, post method, along with input type(text) and name(mood) will be sent to the server. Since we will be saving user input only upon form submission, we will write the following if statement.

If the url and method of incoming request are /mood and post respectively, then save the user input in a separate file.

# in routes.js

 if (url === "/mood" && method === "POST") {
        //rest of the code
   }
Enter fullscreen mode Exit fullscreen mode

Instead of waiting until full incoming messages are read into memory, Node.js handles data in chunks. We will accomplish this by writing an event listener that listens for data.

In Node.js, event listeners are initiated by req.on(). The first parameter specifies the name of the event and the second parameter defines the function triggered by an event.

In the code below, we create an array called body as we are getting data from the request body. Then, we create an event listener that listens for incoming data. As soon as chunk of data is detected, it pushes the chunk into the body array.

# in routes.js

 const body = [];
    req.on("data", (chunk) => {
      body.push(chunk);
    });
Enter fullscreen mode Exit fullscreen mode

We will now create an end listener. The end listener will fire once it is done parsing the incoming request data.

# in routes.js

 return req.on("end", () => {
      const parsedBody = Buffer.concat(body).toString();
      console.log(parsedBody)
    });
Enter fullscreen mode Exit fullscreen mode

We have previously pushed chunks of data in a body array. To interact with these chunks of data, we first need to group the chunks in the body array into a buffer(Buffer.concat(body)).

Buffer now has to be turned into a string(.toString()) so that our code can work with the data! We will set the result equal to parsedBody.

Let's console.log the parsedBody to see what we are working with here.

Quit and start your server and refresh your browser. In the form, type in "Excited" and submit the form.

You will notice that your browser url will change to localhost:3000/moods and display a blank page. This makes sense as we do not have any html code written for /moods url.

Go back to the server terminal, you will see the following in your terminal.

# in terminal

mood=Excited
Enter fullscreen mode Exit fullscreen mode

This means that the form is capturing user input and is sending it our server in request body. But we only want the mood value "Excited" to be saved in our file.

# in routes.js

const mood = parsedBody.split("=")[1];
Enter fullscreen mode Exit fullscreen mode

We can achieve that by splitting parsedBody(mood=Excited) by =. This will yield an array of ["mood", "Excited"]. We can further isolate "Excited" by specifying that we want element at index position of 1 and saving that as a mood constant.

Next, we can create a file to store user input. At the very top of routes.js file, we require fs package and set it to a fs constant.

#In routes.js at the very top of the file

 const fs = require("fs");
Enter fullscreen mode Exit fullscreen mode

Right after const mood = parsedBody.split("=")[1], copy and paste the following.

fs.writeFile("user_mood.txt", mood, () => {});
      return res.end();
Enter fullscreen mode Exit fullscreen mode

At the very top of route.js, we have imported fs package. This package contains writeFile functionality which allows us to create a file and add whatever information we want to save.

fs.writeFile takes in two arguments. The first argument is the file name, "user_mood.txt". The second argument is what you want to add to the file. We will include our mood variable which contains "Excited" as its value.

Lastly, we use res.end() function to end the response process.

Let's test it out!

Stop the server and fire up the server. Go to your browser and fill out your mood in the form and hit send.

Go back to your server. You will see that a file named user_mood.txt has been created in your server. Go into the file and you will see that Excited has been saved in the file!
Alt Text

There you have it! This blog was full of complex concepts and coding. Major kudos to you for making it to the end.
alt text

Now go apply what you have learned and add more routes and features!

Discussion (22)

Collapse
jornvm profile image
Jornvm

Great writing for educational purposes! Going trough this was my first encounter with both node.js, backend and using wsl 2. I got stuck on the last part, console returns my input but does not create the user_mood.txt.
I also get a DeprecationWarning for calling an asynchrounous function without callback, so i guess im going to be spending my saturday figuring that out :)

Thank you for this guide :D

Collapse
crispy1260 profile image
Christopher Payne

Hey Jornvm,

When I was running through this, I got the following error in my console referencing the fs.writeFile section.

mood=Excited
fs.js:144
  throw new ERR_INVALID_CALLBACK(cb);
  ^
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined
    at maybeCallback (fs.js:144:9)
    at Object.writeFile (fs.js:1325:14)
    at IncomingMessage.<anonymous> (E:\Users\Christopher\Documents\GitHub\All_The_Feels\routes.js:30:10)
    at IncomingMessage.emit (events.js:327:22)
    at endReadableNT (_stream_readable.js:1221:12)
    at processTicksAndRejections (internal/process/task_queues.js:84:21) {
  code: 'ERR_INVALID_CALLBACK'
}

In my troubleshooting, I added some error handling and it started working for me. I don't understand why adding error handling to that function helped but it allowed the file to be created.

Looking at Lisa's Github repo (linked below), my line 24 of routes.js now looks like:

fs.writeFile("user_mood.txt", mood, function (err) {
    if (err) {
        return console.log(err);
    }
    console.log("File saved successfully!");
});

Hopefully, this gets it to work for you too! I assume it's a copy/paste fail on my part and I am overlooking the error in my file but this worked for me.

Collapse
malanius profile image
Malanius Privierre

Hi, this probably depends on the version of Node the post was written and version of Node you're using. The callback parameter is required since Node.js v10.0.0, see docs history where this is mentioned: nodejs.org/api/fs.html#fs_fs_write...

Thread Thread
lisahjung profile image
Lisa Author

Malanius! Thank you for sharing your insights with everyone. One of the many things I love about the developer community is how generous and invested they are in other developers' growth. It feels really good to see readers helping other readers!! :)

Collapse
lisahjung profile image
Lisa Author

Hey Christopher!
Thank you so much for taking the time to share your troubleshooting tips with everyone. I appreciate you!!

Collapse
lisahjung profile image
Lisa Author

You are so welcome Jornvm. I am just getting started on Node.js myself and it's so exciting to hear that you are delving into it as well.

Hm... I haven't encountered that issue while I was building the server. Have you tried comparing your code to the my GitHub repo for this tutorial? github.com/LisaHJung/Node.js_Tutor...

You probably figured it out already lol but let me know how it went!

Collapse
andrewbaisden profile image
Andrew Baisden

Cool guide just a few things. You can add syntax highlighting to your code blocks in markdown it will make the article easier to read. Also when working with API's its good practice to use an API tool like postman.com/ or insomnia.rest/

Collapse
lisahjung profile image
Lisa Author

Thank you so much for your great advice,Andrew! I totally agree about using Postman. I usually use Postman but wanted to shake it up this time and try something different. lol I had no idea you can add syntax highlighting to my code blocks in markdown. I will Google how to do it and apply it to my blog next time!

Collapse
james245332 profile image
James245332

OK so just send me your hangout email so we can talk

Collapse
thomassalty profile image
Thomas Soos

I know it's a beginners' guide but how should I imagine an experts' guide? Is this what professional back-end developers do most of time? Writing these "server.js" and "routes.js" files? I'm just trying to understand what it looks like in the real world, outside of "localhost:3000"...

Collapse
lisahjung profile image
Lisa Author

Hey @thomas Soos! What a great question. Professional back end developers are certainly expected to have mastered the skill sets covered in this blog. However, these skills barely scratch the surface of what you do as a back end developer!

The responsibilities of a back end developer may differ depending on the company he/she works for. In general, back end developer focuses on the logic necessary to make the app function quickly and efficiently. They also develop and manage database to store and secure info needed to run the app. Also, adding Auth and other security measures to your app are definitely important aspects of keeping your app secure.

Some of the skill sets that professional back end developers may focus on are security compliance and accessibility. database administration, managing version control and also the ability to work with front end developers to ensure that front and back end of the app are working together seamlessly.

Hope this helps!

Collapse
thomassalty profile image
Thomas Soos

Thanks a lot for the quick reply @lisahjung !

It certainly helps, but now I've got 3 more questions if you don't mind me asking 馃榾

1, I've thought accessibility is something that front-end developers work on. For example using semantic html, using alt texts for images and aria-labels for screen readers, etc, etc. Or, if you meant accessibility in terms of speed and performance of a website, I think that's also a front-end developer's responsibility by keeping the number of requests low, minifying and compressing necessary files, using CDNs, etc. So my question is:
What do you mean by "back end developers may focus on accessibility"? What do they do to improve it?

2, What if Auth and other security measures have already been added as well as a database? Or even when there's no database? I understand, that no website or webapp is "done" but what if these security measures have been added, they work fine and there are no security issues? What do back-end developers do in that case? Are they just going to start to work on a new app or website?

3, How does a node.js server look like in the real world? Should I imagine a normal server computer that has a more complex "server.js" stored in it? What port does it listen to? If it also listens to port 3000 or any other port, why don't we have to add the port number on URLs the same way we did with localhost:3000?

Once again, thanks a lot for your help! I do appreciate it 馃槈

Collapse
dmartensson profile image
David M氓rtensson

I have one objection to the description.

You write that javascript runs on the V8 engine, but you fail to clarify that this is just one possible engine.
If I as a reader know nothing about it I would get the impression that javascript was invented by google and that V8 is the only way to run it :)

I know its under note.js history, but I would clarify that node.js selected the V8 engine from google to run Javascript out of the different existing ones.

Collapse
lisahjung profile image
Lisa Author • Edited on

@david Martensson! Thank you so much for letting me know. I am still new to Node.js and I have a lot to learn! I made the change. Thanks again. :)

Collapse
malanius profile image
Malanius Privierre

Thank you for the excellent post. I've been preparing a presentation for our regular back-end chapter meeting. Although working with Node for a long time, it was not easy to sum it up for devs that grew on java based backends.
Your post and beginner view helped me a lot to put it together! 馃槉

Collapse
lisahjung profile image
Lisa Author

Oh wow! You are so welcome Malanius. Your post put a huge smile on my face. Thank you so much for such a wonderful message and for inspiring me to continue writing. Hope you have the most wonderful holiday weekend! :)

Collapse
tracy02022 profile image
DONGYU LI

Hi Lisa,
Very like your post, but seems like you forget to set a callback function there for fs writer ,
I am fixing it like this
fs.writeFile("user_mood.txt", mood, () => {});
Thanks,
Tracy

Collapse
lisahjung profile image
Lisa Author • Edited on

@Dongyu LI, Tracy!!! Thank you so much for your help. I just made the change you suggested in my blog and in my GitHub repo. Sorry for the delayed response. I cannot believe I didn't see your comment till now.

Thank you so much for helping me and my readers. You are wonderful!!

Collapse
hugofeijo profile image
Hugo Feij贸

incredible writing, congratulations

Collapse
lisahjung profile image
Lisa Author

Oh wow, thank you so much for such a wonderful compliment Hugo!! You made my day. :)))))

Collapse
ladyfantasy profile image
Lady Fantasy

Great tutorial, very useful! Thank you!

Collapse
lisahjung profile image
Lisa Author

You are so welcome Soledad. I am so glad I could help!! :)