Node.js http and https Modules: Servers, Requests, and TLS

Node.js includes built-in http and https modules for creating web servers and making network requests without extra packages. This article explains how both modules work, how they differ, and how to use them correctly in real Node.js programs.

Quick answer: Use http for plain-text connections and https when you need TLS encryption. Their APIs are very similar, but https adds certificate handling and secure defaults.

Difficulty: Beginner

You'll understand this better if you know: basic JavaScript functions, how objects and callbacks work, and the difference between a client request and a server response.

1. What Are Node.js http and https?

The http and https modules are core Node.js modules used for low-level web communication. They let you:

http uses unencrypted HTTP traffic. https uses HTTP over TLS, which encrypts data in transit and verifies the server with certificates.

These modules are a good fit when you want direct control over headers, streaming, and connection handling. They are lower-level than frameworks like Express, so you work closer to the actual protocol.

2. Why Node.js http and https Matter

Most web applications depend on HTTP, even if they use higher-level frameworks. Understanding these modules helps you debug server issues, build simple services, and reason about what frameworks are doing behind the scenes.

They matter because they provide the foundation for:

They also help when a framework fails to explain a problem clearly. If you understand the native modules, errors such as connection refusals, missing headers, or certificate problems become easier to diagnose.

3. Basic Syntax or Core Idea

The simplest http server uses createServer with a request listener. The listener receives a request object and a response object.

Minimal HTTP server

This example returns plain text to every request.

const http = require('node:http');

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

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

Here, req contains details about the incoming request, and res is used to build the outgoing response. The server listens on port 3000.

Minimal HTTPS server

An HTTPS server works the same way, but it needs a certificate and private key.

const https = require('node:https');
const fs = require('node:fs');

const options = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem')
};

const server = https.createServer(options, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Secure hello');
});

server.listen(3443, () => {
  console.log('HTTPS server running at https://localhost:3443');
});

The structure is familiar, but the server can only start after Node can read valid TLS files.

4. Step-by-Step Examples

Example 1: Read the request method and URL

In a basic HTTP server, the request object tells you what the client asked for. This is useful for routing.

const http = require('node:http');

http.createServer((req, res) => {
  console.log(req.method, req.url);
  res.end('OK');
}).listen(3000);

This prints values like GET / or POST /api/users. That information is the starting point for most routing logic.

Example 2: Return JSON from a server

Many APIs return JSON instead of plain text. You must set the correct content type and serialize the body yourself.

const http = require('node:http');

http.createServer((req, res) => {
  const data = {
    status: 'ok',
    time: new Date().toISOString()
  };

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}).listen(3000);

The response body must be a string or buffer, so JSON.stringify is required before calling end.

Example 3: Make an HTTP client request

The same module can also make outbound requests. This is useful for calling APIs, internal services, or webhook endpoints.

const http = require('node:http');

const request = http.request({
  hostname: 'example.com',
  path: '/',
  method: 'GET'
}, (res) => {
  let body = '';

  res.on('data', (chunk) => {
    body += chunk;
  });

  res.on('end', () => {
    console.log(body);
  });
});

request.end();

This pattern shows the streaming nature of Node.js HTTP clients. Data may arrive in chunks, so you collect it before using it.

Example 4: Make an HTTPS client request

Using https as a client is almost identical, but the connection is encrypted and certificate checks apply.

const https = require('node:https');

https.get('https://example.com', (res) => {
  let body = '';

  res.on('data', (chunk) => {
    body += chunk;
  });

  res.on('end', () => {
    console.log(body);
  });
}).on('error', (err) => {
  console.error(err.message);
});

The request flow is familiar, but secure transport can fail if the certificate is invalid or untrusted.

5. Practical Use Cases

These modules are especially useful when you need direct access to the protocol instead of framework abstractions.

6. Common Mistakes

Mistake 1: Using the wrong module for the URL scheme

If the target URL starts with https, you must use https or a client configured for TLS. Using http against an HTTPS-only server usually fails.

Problem: The client tries to talk plain HTTP to a secure server, which can produce connection resets or protocol errors.

const http = require('node:http');

http.get('https://example.com', (res) => {
  console.log(res.statusCode);
});

Fix: Use the matching module for the protocol.

const https = require('node:https');

https.get('https://example.com', (res) => {
  console.log(res.statusCode);
});

The corrected version works because the TLS handshake is handled properly.

Mistake 2: Forgetting to end the response

An HTTP server must close each response. If you send headers but never call end, the client may hang waiting for more data.

Problem: The request never completes because the response stream stays open.

const http = require('node:http');

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('Hello');
}).listen(3000);

Fix: Call end after writing the body, or pass the body directly to end.

const http = require('node:http');

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

The corrected version works because the server clearly signals that the response is complete.

Mistake 3: Sending a JavaScript object directly in the response

The response body must be a string, buffer, or stream. A plain object is not a valid response payload.

Problem: Node.js cannot serialize a plain object for you, so this often throws a type error or sends the wrong body.

const http = require('node:http');

http.createServer((req, res) => {
  res.end({ message: 'Hi' });
}).listen(3000);

Fix: Convert the object to JSON and set an appropriate content type.

const http = require('node:http');

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hi' }));
}).listen(3000);

The corrected version works because the response body is a valid JSON string.

7. Best Practices

Practice 1: Set status codes and headers explicitly

Clear status codes and headers make your server easier to debug and consume. Do not rely on defaults when the response has meaningful semantics.

const http = require('node:http');

http.createServer((req, res) => {
  res.writeHead(404, { 'Content-Type': 'text/plain' });
  res.end('Not found');
}).listen(3000);

This makes failures easier to understand for both humans and client code.

Practice 2: Handle request and response errors on client calls

Outbound requests can fail because of DNS issues, refused connections, timeouts, or TLS problems. Always listen for errors.

const https = require('node:https');

const request = https.request('https://example.com', (res) => {
  res.resume();
});

request.on('error', (err) => {
  console.error('Request failed:', err.message);
});

request.end();

This prevents your process from failing silently when the network is unavailable.

Practice 3: Keep HTTPS private keys out of source control

TLS key material should not be committed to a repository. Load it from files, environment-specific secrets, or a deployment platform’s secret store.

const fs = require('node:fs');

const options = {
  key: fs.readFileSync(process.env.TLS_KEY_PATH),
  cert: fs.readFileSync(process.env.TLS_CERT_PATH)
};

This reduces the risk of accidental leaks and makes deployment safer.

8. Limitations and Edge Cases

If you need routing, cookie parsing, or middleware, you can build those pieces yourself on top of these modules or use a framework that wraps them.

9. Practical Mini Project

Here is a small but complete service that exposes two endpoints: a health check and a JSON time endpoint. It uses only the built-in http module.

const http = require('node:http');

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
    return;
  }

  if (req.method === 'GET' && req.url === '/time') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      time: new Date().toISOString()
    }));
    return;
  }

  res.writeHead(404, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'Not found' }));
});

server.listen(3000, () => {
  console.log('Listening on http://localhost:3000');
});

This project shows the core pieces together: request inspection, branching on path and method, JSON responses, and a fallback 404.

10. Key Points

11. Practice Exercise

Expected output: Visiting / should show a text greeting, visiting /status should show a JSON object, and any unknown path should return a 404 response.

Hint: Check both req.method and req.url before choosing which response to send.

Solution:

const http = require('node:http');

http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello, world!');
    return;
  }

  if (req.method === 'GET' && req.url === '/status') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true }));
    return;
  }

  res.writeHead(404, { 'Content-Type': 'text/plain' });
  res.end('Not found');
}).listen(3000);

This solution works because each route sends a complete response with the correct type and status code.

12. Final Summary

The Node.js http and https modules are the core building blocks for web servers and outbound network requests. They expose the protocol directly, which makes them simple in concept but powerful in practice.

http is used for unencrypted communication, while https adds TLS security and certificate handling. Their APIs are closely related, so once you learn one, the other feels familiar.

For real-world work, these modules are best when you want fine-grained control or need to understand what higher-level frameworks are doing. If you plan to build larger applications, the next step is to learn routing, middleware, and streaming patterns on top of these core APIs.

}