In this tutorial we will learn about HTTPS and TLS in Node.js. In our HTTP tutorial, we learned how a client sends a request to a server and how the server returns a response. We also explored how Node.js parses HTTP messages, handles request bodies through streams, and constructs responses using the ServerResponse object.
But there is a fundamental problem with ordinary HTTP: the communication is not encrypted by the protocol itself.
Imagine a user logging into a website. The browser sends credentials to the server, and the server receives them. If that communication travels over ordinary HTTP, an attacker who can observe the traffic along the network path may be able to read those credentials.
The problem extends beyond passwords. Requests can contain cookies, session tokens, private messages, payment information, and other sensitive data. Responses can reveal personal information or application data as well.
Encryption is necessary, but encryption alone is not the entire solution.
Suppose you connect securely to a server, but the server is actually an attacker’s machine impersonating the real website. Even if your communication is encrypted, you may still be sending your data to the wrong destination.
We therefore need a mechanism that addresses three different security requirements:
Confidentiality: Other parties should not be able to read the protected communication.
Integrity: An attacker should not be able to modify protected data without detection.
Authentication: The client should be able to verify the identity of the server it intended to contact.
This is the problem solved by HTTPS using TLS.
In this tutorial, we will understand the cryptographic ideas that make TLS work, follow the handshake that establishes a secure connection, explore digital certificates and certificate authorities, and build an HTTPS server using Node.js’s built-in node:https module.
We will not use Express. Our goal is to understand the security layer that operates beneath our application code.
Table of Contents
Part 1: From HTTP to HTTPS
1. What does HTTP actually transmit?
Consider a simple HTTP request:
GET /account HTTP/1.1
Host: example.com
Cookie: session=abc123
The request contains readable text. The same is true of an ordinary HTTP response:
HTTP/1.1 200 OK
Content-Type: text/plain
Welcome back!
These are simplified representations of HTTP/1.1 messages, but they illustrate the issue.
HTTP defines the request and response format. It does not encrypt the message contents.
If an attacker can observe the unencrypted traffic on a vulnerable network path, HTTP itself does not prevent that attacker from reading the request or modifying its content.
A secure transport layer is needed to protect the HTTP exchange.
2. What is HTTPS?
HTTPS stands for Hypertext Transfer Protocol Secure.
It is HTTP carried over TLS, the Transport Layer Security protocol.
The Node.js documentation describes HTTPS in exactly this architectural sense: HTTP over TLS, implemented through the separate node:https module. See the Node.js HTTPS documentation.
The difference is not that HTTP suddenly becomes a different application protocol. The HTTP messages retain their meaning, but TLS protects those messages as they travel between the endpoints.
The simplified architecture changes from:
HTTP:
Application
|
v
HTTP Messages
|
v
TCP Connection
|
v
Network
to:
HTTPS:
Application
|
v
HTTP Messages
|
v
TLS Security Layer
|
v
Protected Data Records
|
v
TCP Connection
|
v
Network
For ordinary HTTPS over TCP, TLS operates between HTTP and the underlying TCP connection.
This explains an important distinction: HTTP defines the application-level request and response, while TLS provides the protected communication channel.
It also connects directly to our earlier tutorials. Buffers represent bytes, Streams manage data flow, and Node.js uses networking APIs to transport those bytes. TLS protects the bytes exchanged through the connection, while the HTTP implementation continues to interpret them as requests and responses.
3. A brief history: SSL became TLS
Before TLS, Netscape developed the Secure Sockets Layer protocol, commonly called SSL, to protect network communication.
SSL was followed by TLS, which standardized and evolved the security protocol.
You may still see terms such as “SSL certificate” or “SSL/TLS certificate” in hosting dashboards and product documentation. In modern deployments, however, the security protocol should be TLS rather than the obsolete SSL versions.
TLS 1.0 and TLS 1.1 are obsolete, and current deployments generally use TLS 1.2 or TLS 1.3, depending on the supported environments and configuration. TLS 1.3 is specified in RFC 8446. MDN also provides an accessible overview of Transport Layer Security.
The central purpose remains the same: create a secure communication channel over an underlying transport that does not itself provide application-level confidentiality and authentication.
Part 2: The Three Security Properties of TLS
Understanding TLS becomes much easier when we examine the problems it solves separately.
1. Confidentiality: Preventing unauthorized reading
Suppose you send this HTTP request:
POST /login HTTP/1.1
Content-Type: application/json
{"username":"riya","password":"secret123"}
Without transport encryption, a party able to capture the relevant traffic may be able to inspect that content.
Under HTTPS, the HTTP message is protected by TLS after the secure session is established.
An observer may still see certain connection metadata, such as the destination IP address, timing, and approximate amounts of traffic. But the HTTP headers and body carried inside the protected TLS application data are encrypted.
Conceptually:
Client wants to send:
{"password":"secret123"}
|
v
TLS Encryption
|
v
Protected TLS Records
|
v
Network
|
v
TLS Decryption
|
v
Server receives:
{"password":"secret123"}
The attacker may observe ciphertext but should not be able to recover the protected plaintext without the necessary cryptographic secrets.
2. Integrity: Detecting modification
Encryption is not enough if an attacker can secretly modify a message.
TLS therefore also protects data integrity.
Modern TLS uses authenticated encryption to ensure that protected records cannot be altered undetectably. When the receiver processes a record, it verifies the authentication information before accepting the plaintext.
If the protected data has been modified, verification fails and the connection cannot safely continue processing that record as valid application data.
This protects the communication from unnoticed tampering in transit.
Integrity is important for ordinary page requests, but it is equally important for API responses, software downloads, payment requests, and other operations in which altered content could cause serious damage.
3. Authentication: Knowing who you are talking to
Now suppose an attacker creates a fake website that looks identical to your bank’s website.
Even if your connection to that fake site is encrypted, confidentiality alone does not solve the problem. The connection would protect your information from outsiders, but it would protect the communication with the wrong server.
TLS addresses this with authentication.
For ordinary HTTPS, the server presents a digital certificate. The client validates that certificate and checks whether it is appropriate for the hostname it intended to contact.
If the certificate is valid and trusted for that hostname, the client gains cryptographic evidence that it is communicating with the server identity represented by the certificate.
This is the basis of server authentication in HTTPS.
Client authentication is also possible through client certificates, but it is optional and much less common for ordinary public websites.
The TLS 1.3 specification describes these properties as the basis of a secure communication channel.
What HTTPS does not guarantee
HTTPS does not prove that a website is honest, that an API is free from vulnerabilities, or that the server stores your information safely.
It also does not automatically authenticate a user to the application. That requires an application-level authentication mechanism such as a session, access token, or another suitable method.
TLS protects communication with the authenticated server endpoint. It does not replace authorization, input validation, secure data storage, or good application design.
Part 3: Why Do We Need Both Symmetric and Asymmetric Cryptography?
At first glance, this may seem unnecessarily complicated.
Why not simply encrypt every HTTP message using a public key? Why do we need a handshake, certificates, and multiple kinds of cryptographic operations?
The answer is that cryptographic systems must solve different problems efficiently.
To understand TLS, we first need to distinguish symmetric encryption from public-key cryptography.
1. Symmetric encryption
Symmetric encryption uses a secret key to encrypt and decrypt data.
Conceptually:
Shared Secret Key
/ \
v v
Plaintext -> Encryption Decryption -> Plaintext
|
v
Ciphertext
Both parties need access to the correct secret key.
Symmetric encryption is efficient, making it suitable for protecting large amounts of application data after a secure connection has been established.
But it presents an initial problem: how do the two parties obtain a shared secret without exposing it to an attacker?
If the client sends the secret key directly over an insecure network, an attacker could intercept it.
TLS solves this problem by using a handshake that establishes shared keying material.
2. Public-key cryptography
Public-key cryptography uses mathematically related public and private keys.
The public key can be shared, while the private key must be protected.
A simplified view is:
Key Pair
/ \
v v
Public Key Private Key
Can Share Must Protect
Different public-key operations serve different purposes.
Some schemes support operations such as encryption and decryption. Others, particularly digital signature schemes, allow someone with a private key to sign data and others to verify the signature using the corresponding public key.
TLS relies on these properties for authentication and key establishment.
An important distinction: modern TLS does not simply encrypt every HTTP message using the server’s public key. Public-key cryptography helps authenticate the server and establish shared secrets. Symmetric keys are then used to protect application data efficiently.
This division of responsibility is fundamental to how modern HTTPS works.
3. How do two parties agree on a secret without sending it directly?
TLS 1.3 commonly uses ephemeral Diffie-Hellman key agreement, often over elliptic curves, to establish shared secret material.
Each side contributes key-exchange information. Using their own secret values and the other side’s public key-exchange information, both sides can derive the same shared secret.
An eavesdropper who sees the exchanged public values should not be able to feasibly calculate the shared secret, assuming the cryptographic algorithms and parameters are secure.
Client Server
| |
Client key share Server key share
| |
+------------ Network ----------+
| |
v v
Client derives secret Server derives secret
| |
v v
Shared secret = same on both sides
The parties do not need to transmit the final secret itself.
TLS uses the handshake messages and a key-derivation process to produce traffic keys from the agreed secret material.
The public-key key-exchange information is not, by itself, proof of server identity. Authentication is a separate part of the handshake, which brings us to certificates.
Part 4: What Is a Digital Certificate?
A digital certificate is a data structure that binds an identity to a public key, together with information such as the issuer, validity period, and usage constraints.
For HTTPS, the server presents a certificate that identifies the hostname or hostnames it is authorized to represent.
Consider a certificate for:
api.example.com
Conceptually, the certificate contains information resembling:
Certificate
|
+-- Subject / Identity information
|
+-- Subject Public Key
|
+-- Validity Period
|
+-- Subject Alternative Names
| |
| +-- DNS: api.example.com
|
+-- Issuer
|
+-- Digital Signature
A real X.509 certificate has a standardized structure. The diagram is a conceptual overview of its important fields.
1. Public key and private key
A server possesses a private key and makes its corresponding public key available through its certificate.
The private key must remain secret. It provides the cryptographic capability required for operations such as proving possession of the key during TLS authentication.
The certificate itself is not secret. The client needs access to it so that it can inspect and validate the server’s identity and public key.
This is why Node.js’s HTTPS server needs both a certificate and the corresponding private key.
2. What does a certificate authority do?
A Certificate Authority, or CA, is an entity trusted to issue certificates according to its validation procedures.
Instead of manually trusting every certificate on the Internet, clients maintain a set of trusted root certificates. The server typically presents a certificate issued by an intermediate CA, which can chain back to a trusted root.
The conceptual chain looks like this:
Client Trust Store
|
v
Root CA
|
v
Intermediate CA
|
v
Server Certificate
|
v
api.example.com
The client’s trust store contains trusted root certificates. The presented chain allows the client to verify the server certificate through the relevant issuing authorities.
The chain does not make the website trustworthy in every possible sense. It provides evidence that the certificate was issued under the CA’s validation process and links the relevant public key to the named identity.
3. What does the browser validate?
When a browser connects to an HTTPS website, it performs certificate validation according to its trust rules and the TLS requirements.
Typical checks include:
Certificate chain: Is the certificate linked to a trusted issuing authority?
Validity period: Is the certificate currently valid?
Hostname: Does the certificate cover the hostname the user intended to visit?
Usage and cryptographic constraints: Is the certificate suitable for the intended server-authentication purpose, and are its signatures and algorithms acceptable?
Proof of private-key possession: Does the server demonstrate possession of the private key associated with the certificate during the TLS handshake?
The hostname check is especially important.
For example, a certificate for api.example.com is not automatically valid for evil.example.net just because both are websites.
Modern certificates use the Subject Alternative Name extension to identify the DNS names or IP addresses covered by the certificate. A certificate’s subject common name alone should not be relied upon as the modern hostname-matching mechanism.
You can learn more about certificate validation and TLS behavior through MDN’s TLS overview.
4. Why does a self-signed certificate cause a warning?
A self-signed certificate is signed using its own private key rather than being issued through a trusted certificate chain.
You can create one and use it for local development, but a typical browser will not automatically trust it merely because it is cryptographically valid.
The browser cannot infer that you are authorized to represent the hostname simply because you generated a certificate for that hostname.
For public services, certificates are normally obtained from a trusted certificate authority. For local development, you can create a local CA and explicitly configure a development client to trust it.
We will do exactly that with our Node.js example.
Part 5: The TLS Handshake, Step by Step
Now we can put the cryptographic pieces together.
When a client connects to an HTTPS server over TCP, the application does not immediately begin exchanging ordinary HTTP messages.
First, the client and server establish a TLS session.
The TLS handshake is the negotiation and authentication process that establishes the parameters and traffic keys for protected communication.
The following diagram illustrates a typical full TLS 1.3 handshake. Exact messages can vary depending on the options and extensions negotiated.
CLIENT SERVER
| |
|------- TCP Connect ------>|
| |
|------ ClientHello ------->|
| |
|<----- ServerHello --------|
| |
|<-- EncryptedExtensions ---|
|<------ Certificate -------|
|<--- CertificateVerify ----|
|<-------- Finished --------|
| |
| Verify server identity |
| |
|-------- Finished -------->|
| |
|<==== Protected Data ======>|
| HTTP messages |
This diagram is simplified. In TLS 1.3, the server’s handshake messages after ServerHello are encrypted using handshake traffic keys. The specific extensions and the exact messages can differ depending on the connection.
The detailed message flow is defined in RFC 8446.
Let’s understand the important stages.
Step 1: TCP connection
For HTTPS over TCP, the client first establishes a TCP connection to the server’s IP address and port.
The conventional port for HTTPS is 443, although our local development server will use port 3443.
The TCP connection provides an ordered byte stream. TLS operates over this stream.
Step 2: ClientHello
The client sends a ClientHello message.
It advertises information such as the TLS versions and cryptographic parameters it supports. It can also include a key share for key agreement and extensions that help establish the connection.
Two important examples are SNI and ALPN.
SNI, or Server Name Indication, allows the client to indicate the hostname it wants to connect to during the TLS handshake. This is useful when a server hosts multiple HTTPS websites and needs to select the appropriate certificate and configuration.
ALPN, or Application-Layer Protocol Negotiation, lets the endpoints negotiate which application protocol will run over the secure connection, such as HTTP/1.1 or HTTP/2.
Neither extension is the same as the HTTP request path. They are part of connection negotiation.
Step 3: ServerHello
The server responds with ServerHello and selects compatible connection parameters.
In a typical TLS 1.3 key-exchange handshake, both sides use their key-exchange information to derive shared secret material.
From this point, the protocol derives traffic keys used to protect subsequent handshake messages and application data.
Step 4: The server presents its certificate
The server sends a certificate chain and the other authentication messages required by the handshake.
The client checks whether the certificate is valid for the intended hostname, chains to a trusted authority, and satisfies the relevant validation rules.
The certificate provides the public key and identity information needed for server authentication.
Step 5: CertificateVerify
In a certificate-based TLS 1.3 handshake, the server sends a CertificateVerify message containing a digital signature over the prescribed handshake context.
This demonstrates possession of the private key corresponding to the certificate’s public key and binds the authentication to the current handshake.
Simply presenting a public certificate would not be sufficient proof that the server possesses the associated private key.
Step 6: Finished
The handshake includes Finished messages that provide key confirmation and protect the integrity of the handshake transcript.
The client verifies the server’s handshake messages and sends its own Finished message.
At the appropriate point in the handshake, the parties have established the keys and parameters needed to protect application data.
Step 7: Protected HTTP communication
Now HTTP requests and responses can be exchanged through the TLS connection.
The application still deals with methods, paths, headers, status codes, and bodies. The TLS layer handles encryption, integrity, and the security properties associated with the session.
The HTTP application does not need to manually encrypt each header or body. Node.js’s HTTPS implementation handles the TLS layer beneath the HTTP API.
How long does the handshake take?
A full TLS 1.3 handshake can normally complete in one network round trip after the transport connection is established. TLS can also support session resumption, which can reduce handshake overhead in certain situations.
The overall connection time still depends on TCP establishment, network latency, cryptographic processing, DNS resolution, and other factors.
TLS 1.3 also defines 0-RTT data for certain resumed sessions, but it has replay-related security considerations. We will not enable or implement it in this tutorial.
The important point is that the handshake establishes security before normal HTTP application traffic is exchanged.
Part 6: How HTTPS Fits into Node.js
In our HTTP tutorial, we used:
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.end("Hello");
});
To create an HTTPS server, we use node:https and supply the TLS credentials required by the server.
Conceptually:
Node.js Application
|
v
HTTP Handler
|
v
Node.js HTTPS API
|
v
TLS Implementation
|
v
TCP Socket
|
v
Network
The important distinction is that the request handler still receives the incoming request and response objects. The application does not need to implement the TLS handshake itself.
Node.js uses the underlying TLS implementation to negotiate the secure connection and protect the data exchanged over it.
This is why the transition from HTTP to HTTPS can be relatively small in the application code, even though the underlying security mechanism is much more sophisticated.
The official Node.js HTTPS API documents the relevant server and client methods.
Part 7: Create a Local Certificate Authority and Server Certificate
Before writing our server, we need a certificate and a private key.
For a real public website, you would normally obtain a certificate from a trusted certificate authority, such as Let’s Encrypt.
For this tutorial, we will create a small local development CA using OpenSSL, then issue a server certificate for localhost and 127.0.0.1.
This approach teaches the certificate chain without asking you to trust an arbitrary self-signed server certificate.
Important: These credentials are for local development only. The local CA private key is highly sensitive because anyone who obtains it could issue certificates trusted by clients configured to trust that CA. Never install this development CA as a trusted root on machines you do not control, and never use these credentials for a public production service.
Step 1: Create a certificate directory
In your project folder, run:
mkdir certs
The directory will contain the local CA files, server key, certificate request, and signed server certificate.
Step 2: Create a local development CA
Run the following command with OpenSSL 3.x:
openssl req -x509 -newkey rsa:3072 -noenc -sha256 -days 3650 \
-keyout certs/dev-ca-key.pem \
-out certs/dev-ca.pem \
-subj "/CN=Node.js Local Development CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
This creates a private key and a self-signed CA certificate.
The dev-ca-key.pem file contains the CA’s private key and must be protected. The dev-ca.pem certificate is the public CA certificate that clients can use when validating certificates issued by this local CA.
The -noenc option leaves the generated private key unencrypted on disk. That is convenient for this local exercise, but it means filesystem permissions are especially important. For production or valuable CA keys, use an appropriate secure key-management process.
Older OpenSSL versions may use -nodes instead of -noenc; check your installed OpenSSL documentation if that option is unavailable.
Step 3: Generate the server private key and certificate request
openssl req -new -newkey rsa:2048 -noenc -sha256 \
-keyout certs/server-key.pem \
-out certs/server.csr \
-subj "/CN=localhost"
This creates the server’s private key and a Certificate Signing Request, or CSR.
The CSR contains the public-key information and identity information that will be signed by the development CA.
The server’s private key is separate from the CA’s private key. The server uses its own key during TLS authentication, while the CA’s key signs the issued certificate.
Step 4: Define the server certificate extensions
Create a file named certs/server.ext with this content:
subjectAltName=DNS:localhost,IP:127.0.0.1
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
These extensions define the certificate’s intended use.
The Subject Alternative Name extension states that the certificate is valid for localhost and the IP address 127.0.0.1.
The CA:FALSE constraint indicates that the server certificate is not a CA certificate. The extended key usage identifies it for server authentication.
The hostname and IP entries are important because certificate validation checks the identity used by the client. A certificate for localhost is not automatically valid for an unrelated hostname.
Step 5: Sign the server certificate
Run:
openssl x509 -req \
-in certs/server.csr \
-CA certs/dev-ca.pem \
-CAkey certs/dev-ca-key.pem \
-CAcreateserial \
-out certs/server-cert.pem \
-days 365 \
-sha256 \
-extfile certs/server.ext
This produces server-cert.pem, a server certificate signed by the local development CA.
Our resulting directory looks like this:
certs/
├── dev-ca-key.pem # CA private key: protect it
├── dev-ca.pem # Public CA certificate
├── dev-ca.srl # Serial number state, created by OpenSSL
├── server-key.pem # HTTPS server private key
├── server.csr # Certificate signing request
├── server-cert.pem # CA-signed server certificate
└── server.ext # Certificate extensions used for signing
The certificate chain for our local setup is:
Local Development CA
|
| Signs
v
localhost Server Certificate
|
v
Node.js HTTPS Server
The client must trust the development CA certificate for normal verification to succeed. Merely possessing the server certificate does not cause a client to trust it.
You can inspect the certificate with:
openssl x509 -in certs/server-cert.pem -text -noout
This displays its subject, issuer, validity dates, public key information, and extensions.
For additional details about the certificate-generation commands, consult the official OpenSSL req documentation.
Part 8: Build an HTTPS Server with Node.js
Now that we have a server key and certificate, we can write the application.
Create https-server.mjs:
import { createServer } from "node:https";
import { readFileSync } from "node:fs";
// Load the server's private key and CA-signed certificate.
// Paths are relative to this script, not the current terminal directory.
const key = readFileSync(new URL("./certs/server-key.pem", import.meta.url));
const cert = readFileSync(new URL("./certs/server-cert.pem", import.meta.url));
const server = createServer({ key, cert }, (req, res) => {
// Parse the incoming request target.
const url = new URL(req.url ?? "/", "https://localhost");
if (req.method === "GET" && url.pathname === "/health") {
// Return a JSON health-check response.
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
});
res.end(JSON.stringify({ status: "secure connection established" }));
return;
}
if (req.method === "GET" && url.pathname === "/hello") {
// Read the optional query parameter.
const name = url.searchParams.get("name") ?? "World";
res.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
});
res.end(`Hello, ${name}!`);
return;
}
// Return 404 when no implemented route matches.
res.writeHead(404, {
"Content-Type": "text/plain; charset=utf-8",
});
res.end("Not Found");
});
// Accept local connections on port 3443.
server.listen(3443, "127.0.0.1", () => {
console.log("HTTPS server listening on https://127.0.0.1:3443");
});
server.on("error", (error) => {
console.error("HTTPS server error:", error.message);
});
Why do we pass key and cert?
The server needs the private key associated with its certificate and the certificate itself.
The certificate supplies the public identity information. During TLS authentication, the server must demonstrate possession of the corresponding private key.
The createServer() method receives the TLS configuration and the HTTP request handler. It returns an HTTPS server that accepts connections and processes requests through TLS.
This is the most important change from ordinary HTTP:
import { createServer } from "node:https";
and:
const server = createServer({ key, cert }, handler);
The request handler remains conceptually similar to our HTTP server. The TLS layer is handled by Node.js.
Start the server
Run:
node https-server.mjs
Expected output:
HTTPS server listening on https://127.0.0.1:3443
The server is now listening for local HTTPS connections.
Part 9: Test the HTTPS Server Correctly
A crucial detail remains: the certificate was issued by our local development CA, which ordinary clients do not trust automatically.
We must tell the client to trust that CA certificate.
1. Using curl
Run:
curl --cacert certs/dev-ca.pem \
"https://127.0.0.1:3443/hello?name=Riya"
The response body should be:
Hello, Riya!
The --cacert option tells curl to use our local CA certificate when validating the server’s certificate.
This means the client can verify the certificate chain and confirm that the certificate covers the requested IP address, rather than simply skipping certificate validation.
Now test the health endpoint:
curl --cacert certs/dev-ca.pem \
"https://127.0.0.1:3443/health"
Response:
{"status":"secure connection established"}
This demonstrates that our HTTPS server is handling HTTP requests over a TLS-protected connection.
2. Why not use curl -k?
You may encounter examples such as:
curl -k https://127.0.0.1:3443/health
The -k option disables normal certificate verification.
It can be useful for diagnosing a local connection, but it defeats an essential part of HTTPS security: verifying the identity of the server.
A connection established with -k may still be encrypted, but the client has deliberately chosen not to enforce the usual trust checks. It should not be treated as evidence that the server’s identity has been authenticated.
For our tutorial, using --cacert demonstrates a much more meaningful setup.
3. What about a browser?
A browser will not necessarily trust our local CA automatically.
For learning purposes, curl is convenient because we can explicitly provide the trusted CA. If you want to use a browser, you would need to configure trust for the development CA in the appropriate local trust store. Do this only on a machine you control, and remove that trust when it is no longer needed.
The server certificate is valid only for the names specified in its Subject Alternative Name extension. A certificate for localhost should not be assumed valid for a different host or public domain.
Part 10: Make an HTTPS Request from Node.js
Node.js also provides a built-in HTTPS client through node:https.
In our HTTP tutorial, we used http.get() and http.request(). Their HTTPS counterparts follow a similar programming model, but the connection is secured with TLS.
Create https-client.mjs:
import { get } from "node:https";
import { readFileSync } from "node:fs";
// Trust the local development CA for this request.
const ca = readFileSync(new URL("./certs/dev-ca.pem", import.meta.url));
const request = get(
"https://127.0.0.1:3443/health",
{ ca },
(res) => {
console.log("Status:", res.statusCode);
// The response is an IncomingMessage readable stream.
res.setEncoding("utf8");
res.on("data", (chunk) => {
console.log("Response:", chunk);
});
},
);
request.on("error", (error) => {
// Network failures and TLS validation failures arrive here.
console.error("HTTPS request failed:", error.message);
});
Run it while the HTTPS server is running:
node https-client.mjs
Expected output:
Status: 200
Response: {"status":"secure connection established"}
The ca option tells the HTTPS client to trust the supplied CA for this connection. The client still checks that the server’s certificate is appropriate for the requested hostname or IP address.
This is a deliberate trust configuration for the local development environment. It is not a general instruction to disable certificate verification or to trust an arbitrary certificate received from a server.
The response body is still consumed as a stream, just as we learned in our HTTP and Streams tutorials.
Part 11: What Happens When TLS Validation Fails?
TLS is useful precisely because it can reject connections that do not meet its security requirements.
Suppose the server presents a certificate that is expired, not trusted, or invalid for the requested hostname.
The client may reject the handshake or report a certificate-related error.
Similarly, if the server is configured with a private key that does not match the certificate, the TLS setup or handshake will fail.
This is expected behavior, not a problem that should automatically be bypassed.
Some common reasons for TLS errors include:
Unknown certificate authority: The certificate chain does not lead to a trusted CA.
Hostname mismatch: The requested hostname or IP address does not match the certificate’s Subject Alternative Name entries.
Expired certificate: The certificate is outside its validity period.
Incomplete certificate chain: The server does not provide the intermediate certificates required for validation, and the client cannot otherwise build a valid chain.
Key or certificate mismatch: The private key does not correspond to the public key contained in the certificate.
The correct troubleshooting approach is to identify which validation condition failed and address it. Disabling validation may hide the symptom while removing the protection that HTTPS is supposed to provide.
For production deployments, use a valid certificate chain, keep the private key secure, configure the server appropriately, and test certificate renewal and expiry processes.
Part 12: What Does TLS Actually Encrypt?
We have established that HTTPS protects HTTP communication, but it is helpful to be precise about what an observer can and cannot see.
After the handshake establishes the TLS session, the HTTP application data is protected by TLS record encryption and integrity mechanisms.
This includes the HTTP headers and bodies carried within those protected application-data records.
However, HTTPS does not conceal every piece of network metadata.
An observer may still see the destination IP address, connection timing, traffic volume, and other characteristics of the connection. DNS resolution can also expose the hostname unless an appropriate encrypted DNS mechanism is used. The hostname may be visible in the TLS Server Name Indication extension unless an additional mechanism such as Encrypted ClientHello is used and supported by the environment.
TLS therefore provides strong protection for message contents, but it should not be described as making all communication metadata invisible.
This distinction matters for realistic threat modeling. Confidential content and network metadata are different things.
Part 13: Mutual TLS: Can the Client Prove Its Identity Too?
In ordinary HTTPS, the server generally authenticates itself to the client using its certificate.
But some systems require the client to authenticate itself at the TLS layer too.
This is known as mutual TLS, or mTLS.
Client Server
| |
|---- Client certificate ----> |
| |
| <--- Server certificate ---- |
| |
| Both verify identity |
| |
| <==== Protected Channel ====>|
In a mutual-TLS configuration, the server requests a client certificate, and the client presents one if it has the required credentials.
The server validates that certificate according to its configured trust policy.
mTLS is common in certain service-to-service systems, enterprise networks, and environments where certificates are used to establish machine identities.
It is not the same as simply sending a username and password through an HTTPS request. TLS-level client authentication happens during connection establishment, while application authentication operates at the HTTP or application layer.
Mutual TLS is an extension of the same ideas we have explored, but we will leave its implementation for a dedicated, more advanced lesson.
Part 14: Important Considerations for Production
Our local server demonstrates how HTTPS works, but secure deployment requires more than calling https.createServer().
Protect the private key
The server private key should not be committed to source control or exposed to untrusted users. In production, private-key storage should follow an appropriate security policy, using suitable access controls and secret-management infrastructure.
Use trusted certificates for public services
A public HTTPS server should present a certificate that clients can validate through an appropriate trust chain and whose names match the public hostname.
For example, an operator may use Let’s Encrypt or another trusted certificate provider.
Plan certificate renewal
Certificates have validity periods. A service must renew certificates before they expire and ensure the server uses the updated certificate.
A certificate that was valid when the application was first deployed can eventually become invalid.
Keep TLS configuration appropriate
Use a modern TLS configuration and avoid obsolete protocol versions and weak cryptographic settings. Node.js relies on its underlying TLS implementation and configuration, so operational choices and runtime versions matter.
Remember that HTTPS does not replace application security
TLS protects data in transit, but the application must still authenticate users where necessary, authorize resource access, validate input, protect stored data, and avoid leaking sensitive information in logs or error responses.
A secure communication channel does not make an insecure application safe.
Conclusion
HTTPS is not simply an HTTP server with a different URL scheme. It is HTTP carried over TLS, which provides a secure communication channel using cryptographic mechanisms for confidentiality, integrity, and authentication.
We began by examining the weaknesses of plain HTTP, then explored symmetric encryption, public-key cryptography, shared-secret establishment, digital certificates, and certificate authorities. We followed the TLS handshake to understand how the client and server establish keys and authenticate the server before exchanging protected HTTP data.
Finally, we generated a local development CA and server certificate, built an HTTPS server using Node.js’s built-in node:https module, and verified the server using both curl and a Node.js HTTPS client.
The key takeaway is that TLS protects the communication channel, while HTTP continues to define the requests and responses exchanged through that channel.
Once these responsibilities are separated in your mental model, the relationship between HTTP, TLS, sockets, certificates, Buffers, Streams, and Node.js becomes much clearer.
Official Documentation and Further Reading
The following resources provide the protocol specifications and API documentation behind the examples in this tutorial.