30 Days of JavaScript: Introduction to Node.js, Day 21

30 Days of JavaScript: Building a Simple Weather App
30 Days of JavaScript: Introduction to Node.js, Day 21

Learn the basics of Node.js, from setting up your environment to building a simple HTTP server. Explore the event loop and create scalable backend applications.

For 20 days you’ve written JavaScript that runs in a browser. Day 21 is where that changes. In our 30 Days of JavaScript series, today we take the same language and run it on the server with Node.js. We’ll get your environment set up, unpack what the event loop actually does, and build a working HTTP server from scratch. By the end you’ll have enough footing to start writing real backend code.

Table of Contents

Setting Up a Node.js Environment

What is Node.js?

Node.js runs the V8 JavaScript engine, the same engine inside Google Chrome, outside of the browser. It’s open source and cross-platform, so the JavaScript you already know now runs on your machine or a server. A Node app runs in a single process and doesn’t spin up a new thread for every request. Instead it leans on asynchronous, non-blocking I/O, which is why it holds up well for real-time work like chat servers, APIs, and streaming.

Installing Node.js

You install Node once and npm, the Node package manager, comes along with it. npm is how you’ll pull in libraries and manage dependencies later, so you don’t need to install it separately.

Step 1: Downloading Node.js

Head to the official Node.js website and grab the installer for your OS (Windows, macOS, Linux). Pick the Long-Term Support (LTS) release. LTS gets a longer support window and fewer surprises, which is what you want while you’re learning.

Step 2: Installing Node.js

Run the installer, click through the prompts, then open your terminal and run:

Bash
node -v

If it prints a version number, Node installed correctly.

Step 3: Installing a Text Editor

Any editor works, but Visual Studio Code is the one most Node developers reach for, mostly for its built-in terminal and debugging. Install it and you’re set to start writing code.

Understanding NPM

npm installs, shares, and manages the dependencies your project needs. You’ll use it constantly. Start a new project like this:

Example:

Bash
npm init -y

That creates a package.json with sensible defaults. It’s the file that records your project’s metadata and every dependency you add.

Understanding the Event Loop

The Event-Driven Architecture

Here’s the idea that makes Node click. When Node hits an I/O operation, reading from the network, a database, or the filesystem, it doesn’t sit there blocking the thread and burning cycles while it waits. It moves on and picks the work back up when the response arrives. That’s how a single process handles many requests at once.

What is the Event Loop?

The event loop is the machinery behind that. It keeps checking the call stack for work to run. When an async operation finishes, its callback gets queued up and the loop runs it when the stack is clear. Here’s the simplest way to see it in action:

Example:

JS
/**
 * Demonstrates the event loop in Node.js.
 *
 * @returns {void}
 */
console.log('Start');
setTimeout(() => {
  console.log('This is an asynchronous message');
}, 2000);
console.log('End');

Start and End print right away. The message inside setTimeout waits its two seconds, then the event loop runs its callback. Node never paused to wait.

Callbacks, Promises, and Async/Await

You’ll manage async work three ways, and it’s worth seeing all three side by side because they’re the same idea dressed differently. Callbacks came first, promises cleaned up the nesting, and async/await reads almost like ordinary top-to-bottom code.

Example with Callbacks:

JS
/**
 * Fetches data with a callback.
 *
 * @param {function} callback - The callback function to execute.
 * @returns {void}
 */
function fetchData(callback) {
  setTimeout(() => {
    callback('Data fetched');
  }, 1000);
}
fetchData((message) => {
  console.log(message);
});

Example with Promises:

JS
/**
 * Fetches data with a promise.
 *
 * @returns {Promise} - A promise that resolves after fetching data.
 */
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data fetched');
    }, 1000);
  });
}
fetchData().then((message) => {
  console.log(message);
});

Example with Async/Await:

JS
/**
 * Fetches data using async/await.
 *
 * @returns {void}
 */
async function fetchData() {
  const message = await new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data fetched');
    }, 1000);
  });
  console.log(message);
}
fetchData();

Same result, three styles. For anything you write today, reach for async/await first. It’s the easiest to read and the easiest to debug.

Building a Simple HTTP Server

Introduction to HTTP Servers in Node.js

One of the first things people build with Node is an HTTP server. It takes requests from clients and sends responses back. Node ships with a built-in http module, so you don’t need a single dependency to get started.

Setting Up a Basic HTTP Server

Let’s start with a server that answers every request with “Hello, World!”.

Example:

JS
/**
 * Creates a basic HTTP server in Node.js.
 *
 * @returns {void}
 */
const http = require('http');
const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World!\n');
});
server.listen(3000, '127.0.0.1', () => {
  console.log('Server running at http://127.0.0.1:3000/');
});

Save it, run it with node, and open http://127.0.0.1:3000/ in your browser. You’ll see “Hello, World!”. That’s a real web server in a dozen lines.

Handling Different Routes

A single response gets old fast. Real servers answer differently depending on the URL. Here you check req.url and branch on it:

Example:

JS
/**
 * Handles different routes in an HTTP server.
 *
 * @returns {void}
 */
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Home Page\n');
  } else if (req.url === '/about') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('About Page\n');
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Page Not Found\n');
  }
});
server.listen(3000, '127.0.0.1', () => {
  console.log('Server running at http://127.0.0.1:3000/');
});

Three paths, three answers, including a proper 404 for anything you didn’t plan for. This is routing in its rawest form, which is exactly what frameworks like Express wrap up for you later.

Serving HTML Content

Plain text is fine for learning, but most apps send HTML. Bring in the fs (file system) module, read a file off disk, and pipe it back as the response:

Example:

JS
/**
 * Serves an HTML file from the HTTP server.
 *
 * @returns {void}
 */
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
  if (req.url === '/') {
    fs.readFile(path.join(__dirname, 'index.html'), (err, data) => {
      if (err) {
        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Internal Server Error\n');
      } else {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/html');
        res.end(data);
      }
    });
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Page Not Found\n');
  }
});
server.listen(3000, '127.0.0.1', () => {
  console.log('Server running at http://127.0.0.1:3000/');
});

Notice the error branch. When the file read fails, you send a 500 instead of crashing. That habit, handling the failure case, is what separates a toy from something you’d actually run.

Handling Query Parameters and POST Requests

Two more things you’ll need constantly: reading data from the URL, and reading data sent in a request body. Query parameters ride along in the URL; POST requests carry data in the body.

Example: Handling Query Parameters

JS
/**
 * Handles query parameters in an HTTP server.
 *
 * @returns {void}
 */
const url = require('url');
const server = http.createServer((req, res) => {
  const queryObject = url.parse(req.url, true).query;
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end(`Query Parameters: ${JSON.stringify(queryObject)}\n`);
});
server.listen(3000, '127.0.0.1', () => {
  console.log('Server running at http://127.0.0.1:3000/');
});

One honest heads-up: url.parse() still works, but Node now marks it legacy and steers you toward the built-in URL class (new URL(req.url, 'http://localhost')) for new code. The old form is fine for a quick demo like this. Keep the newer API in mind for anything real, especially with untrusted input.

Example: Handling POST Requests

JS
/**
 * Handles POST requests in an HTTP server.
 *
 * @returns {void}
 */
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.method === 'POST') {
    let body = '';
    req.on('data', chunk => {
      body += chunk.toString();
    });
    req.on('end', () => {
      res.end(`Received POST Data: ${body}\n`);
    });
  } else {
    res.end('Send a POST request to see data handling in action\n');
  }
});
server.listen(3000, '127.0.0.1', () => {
  console.log('Server running at http://127.0.0.1:3000/');
});

A POST body doesn’t arrive all at once. It comes in chunks, so you collect them on data and don’t act until end fires. That streaming pattern is the same whether you’re taking a form submission or a file upload.

Conclusion

Today you got Node.js running, saw why the event loop lets a single process juggle many requests, and built an HTTP server that routes, serves HTML, and reads request data. That’s the core of backend JavaScript. Everything heavier, Express, databases, real APIs, is built on exactly these pieces.

The thing worth holding onto: Node is the same JavaScript you already know, just aimed at the server. The non-blocking model takes a minute to feel natural, but once it does, full-stack work stops being two languages and becomes one.

Keep poking at the standard library and the npm ecosystem between now and next time. Next lesson we switch focus to JavaScript Design Patterns and how to keep growing code clean and maintainable.

What’s Next?

On Day 22 we get into JavaScript Design Patterns, where you’ll put the Module, Singleton, and Observer patterns to work. See you there.

Next: 30 Days of JavaScript: JavaScript Design Patterns, Day 22

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top