Creating a Basic Web Server with Node.js

So far, we've explored built-in modules like fs and path and created a CLI tool. Now, we're leaping forward by creating a basic web server. This is where I think Node.js truly shines, enabling us to build scalable web apps.

http Module

At the core of Node.js web development lies the powerful http module. This built-in module is your gateway to the world of web servers, allowing Node.js to transfer data over the Hyper Text Transfer Protocol (HTTP) - the foundation of data communication on the World Wide Web.

Let's start our journey by creating the simplest possible web server. Of course, that means "Hello world!" time:

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, World!');
});

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

Let's break this down piece by piece:

First, we import the http module using require(). This gives us access to all the HTTP functionality we need.

Next, we create a server using http.createServer(). This method is the heart of our server. It takes a callback function, often called the request handler, which receives two crucial parameters:

  • req: This object is a treasure trove of information about the incoming HTTP request. It tells us about the URL requested, the HTTP method used, any headers sent, and much more.
  • res: This object is our toolkit for crafting the HTTP response. We use it to send data back to the client, set status codes, and define response headers.

Inside our callback function, we're doing two main things:

  1. We use res.writeHead() to write the response header. We're sending a 200 status code, which means "OK" in HTTP speak, and we're telling the client to expect plain text content.

  2. We use res.end() to send the response body and close the connection. In this case, we're sending the classic "Hello, World!" message.

Finally, we told our server to listen to port 3000. When we run this script, Node.js will keep it running, waiting for incoming connections on this port.

To bring your server to life, save this code in a file (let's call it server.js) and run it with Node.js:

node server.js

Now, open your favorite web browser and navigate to http://localhost:3000. Voila! You should see your "Hello, World!" message proudly displayed. Congratulations, you've just created your first web server!

Creating Routes

Our current server is like a friendly but simple robot — it says the same thing to everyone who visits. In a real-world application, we want our server to be smarter and provide different responses for different requests. This is where routing comes into play.

Let's evolve our server to handle different routes:

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Welcome to the Home Page!');
  } else if (req.url === '/about') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Learn more about us on this About Page.');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Oops! Page not found. (404 Error)');
  }
});

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

In this upgraded version of our server:

  • When someone visits the root URL ('/'), they're greeted with a welcome message to the Home Page.
  • If they navigate to '/about', they'll see a message about learning more on the About Page.
  • We send a 404 error for any other URL, letting them know the page wasn't found.

We're using the req.url property to check which URL the client requested. This allows us to create this simple but effective routing system.

Sending Different Types of Responses

So far, our server has been speaking in plain text. But the web is a rich, multimedia environment. Let's teach our server to communicate in HTML and JSON as well:

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>Welcome to our Website!</h1><p>Enjoy your stay.</p>');
  } else if (req.url === '/api/data') {
    const data = { message: 'This is JSON data', number: 42 };
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(data));
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('404 Not Found - The page you requested doesn\'t exist.');
  }
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000/');
});
  • The home route ('/') now sends an HTML response. This allows us to structure our content with HTML tags, which the browser will render properly.
  • We've added a new route '/api/data' that sends a JSON response. This is particularly useful for building APIs that other applications can consume.
  • We use JSON.stringify() to convert our JavaScript object into a JSON string. This is necessary because we can only send strings or buffers as HTTP responses.

Handling POST Requests

So far, our server has been a great talker, but not much of a listener. Let's change that by handling POST requests, allowing clients to send data to our server. A "POST" request is a verb we use when sending data to a server, typically used for creating new resources or submitting form data.

Let's update our server to handle POST requests:

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/api/message') {
    let body = '';
    req.on('data', chunk => {
      body += chunk.toString();
    });
    req.on('end', () => {
      try {
        const message = JSON.parse(body);
        console.log('Received message:', message);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ status: 'Message received!', echo: message }));
      } catch (error) {
        res.writeHead(400, { 'Content-Type': 'text/plain' });
        res.end('Invalid JSON');
      }
    });
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('404 Not Found - This endpoint doesn\'t exist.');
  }
});

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

This server now handles POST requests to '/api/message':

  • We first check if the request method is POST and the URL is '/api/message'.
  • We then listen for 'data' events and accumulate the request body. This is necessary because the body might be sent in chunks.
  • When the request ends, we parse the JSON body, log it, and send a response echoing the received message.
  • We've also added error handling in case the received data isn't valid JSON.

Testing with cURL

cURL is a command-line tool for transferring data using various protocols. It's incredibly useful for testing HTTP servers. Let's use cURL to test our POST endpoint.

First, make sure your server is running:

node server.js

Now, open a new terminal window to run cURL commands. Here's how to send a POST request to our server:

curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello, Server!"}' http://localhost:3000/api/message

Let's break down this command:

  • -X POST: Specifies that we're sending a POST request.
  • -H "Content-Type: application/json": Sets the Content-Type header to indicate we're sending JSON data.
  • -d '{"message": "Hello, Server!"}': This is the data we're sending in the body of the request. It's a JSON object with a "message" key.
  • http://localhost:3000/api/message: This is the URL of our endpoint.

If everything works correctly, you should see a response like this:

{"status":"Message received!","echo":{"message":"Hello, Server!"}}

You can try sending different JSON data:

curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice", "age": 30}' http://localhost:3000/api/message

Response:

{"status":"Message received!","echo":{"name":"Alice","age":30}}

Testing Error Handling

Let's also test our error handling by sending invalid JSON:

curl -X POST -H "Content-Type: application/json" -d '{invalid json}' http://localhost:3000/api/message

You should receive the "Invalid JSON" error message we set up.

Testing Non-Existent Endpoints

Finally, let's test what happens when we send a request to an endpoint that doesn't exist:

curl -X POST http://localhost:3000/api/nonexistent

You should see the "404 Not Found" message.

By using cURL, we can easily test different scenarios without needing to set up a frontend or use more complex tools. It's a great way to debug and verify your server's behavior directly from the command line.

Remember, while we're using cURL here for its simplicity and ubiquity, in a real-world scenario, you might also use tools like Postman for more complex API testing, or write automated tests using frameworks like Jest or Mocha. However, understanding how to use cURL gives you a powerful tool for quick tests and debugging.

Web Development with Node.js Frameworks

While building a web server from scratch using Node.js's http module is an excellent way to understand the fundamentals, it's important to note that this isn't typically how most developers build web applications. The approach we've explored is somewhat low-level and can become cumbersome for larger, more complex applications.

In real-world scenarios, developers often use web application frameworks that abstract away much of the boilerplate code and provide additional features. These frameworks make it easier to build robust, scalable web applications. Here are a few popular choices:

Express.js: Often considered the standard framework for Node.js web applications. It's minimal, flexible, and widely used.

Hono: A small, simple, and ultrafast web framework for the Edges (Cloudflare Workers, Fastly Compute@Edge, Deno, Bun, Node.js). It's gaining popularity for its performance and ease of use.

Koa: Designed by the team behind Express, Koa aims to be a smaller, more expressive, and more robust foundation for web applications and APIs.

Fastify: Focuses on providing the best developer experience with the least overhead and a powerful plugin architecture.

These frameworks offer features like:

  • More intuitive routing
  • Built-in middleware support
  • Better error handling
  • Simplified request parsing
  • Enhanced security features
  • Easier integration with databases and other services

As you continue your journey in Node.js development, exploring these frameworks will help you build more efficient, maintainable, and scalable web applications. Each has its own strengths and is suited for different types of projects, so it's worth exploring them to find which fits your needs best.

You did it!

Okay that was a long section! You've just taken your first steps into the vast world of web development with Node.js. We've covered the fundamentals of creating a web server:

  • Using the http module to create a server
  • Setting up basic routing to handle different URLs
  • Sending various types of responses (plain text, HTML, JSON)
  • Handling POST requests and parsing request bodies

This is just the beginning of what you can do with Node.js for web development. As your applications grow more complex, you might want to explore frameworks like Express.js, which provide more robust routing and middleware systems.

NodejsBeginner
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.