DEV Community

Ömer Gülen
Ömer Gülen

Posted on • Updated on

Node, Express, SSL Certificate: Run HTTPS Server from scratch in 5 steps

Node, Express, SSL Certificate: Run HTTPS Server from scratch in 5 steps

I've decided to write about this tutorial after I struggled while I was coding one of my web apps for a customer. It was a simple chart for the web but it was collecting data on a Telegram Bot. Held the data in MongoDB and prepared a quick API for fetching the data but got many problems on the way and SSL Certificate was one of them.

So in this tutorial, I will go through my mistakes & problems and my solutions to them, if you want to skip straight to the short version, you can continue from here.

In this article I will not mention MongoDB related code or problems.

1. Creating My Basic API Server with Express

In my projects, I prefer creating an npm or yarn environment after creating the project folder.

So, I've done it with the following commands:

mkdir my-project && cd my-project
yarn init
Enter fullscreen mode Exit fullscreen mode

Just spammed Enter after yarn init and created the project environment with default settings.

(I prefer yarn over npm if there are no obstacles to use it.)

Then, I installed express to my project, locally with:

yarn add express
Enter fullscreen mode Exit fullscreen mode

You can also use:

npm install express
Enter fullscreen mode Exit fullscreen mode

Then, I created my single source file index.js and inserted these lines below:

// import express
const express = require('express');

// create new express app and assign it to `app` constant
const app = express();

// server port configuration
const PORT = 8080;

// create a route for the app
app.get('/', (req, res) => {
  res.send('Hello dev.to!');
});

// server starts listening the `PORT`
app.listen(PORT, () => {
  console.log(`Server running at: http://localhost:${PORT}/`);
});
Enter fullscreen mode Exit fullscreen mode

So far, I imported the express package, created an instance of it and assigned it to the app. Set my PORT variable, and created a route for endpoint handling in my API Server and called app.list(PORT, callback()) method to start my server listening on the specified port.

Went back to my terminal and executed the command below in my project directory:

node index.js
Enter fullscreen mode Exit fullscreen mode

which starts my server and logs to the console as below:

Server running at http://localhost:8080/
Enter fullscreen mode Exit fullscreen mode

Then, I switched to my browser and browsed to http://localhost:8080/ and the following page appeared:

Alt Text

So far so good. My app is correctly listening to my port.
Afterwards, I've tested my initial trial works and wanted to test if I can handle more endpoints. So I've just added another route to my code.

app.get('/omergulen', (req, res) => {
  res.send('Hello Omer! Welcome to dev.to!');
});
Enter fullscreen mode Exit fullscreen mode

I expect this to work only when I entered /omergulen endpoint in my browser.

So, I've stopped my running server with Control+C and re-started again, since hot-reloading is not inherent with how I run my app. Switched to my browser and visited the http://localhost:8080/omergulen and it was working, to be sure I re-visited the http://localhost:8080/ and it was also working as expected.

Alt Text

2. Why and how to use middleware with Express?

After my first API server deploys, I switched to my web app project and sent a fetch request to my API endpoint.

fetch('MY_API_URL')
  .then(function (response) {
    console.log(response);
    return response.json();
  })
  .then(...);
Enter fullscreen mode Exit fullscreen mode

Nothing was happening in my DOM, but the console message was frustrating.

Access to fetch at 'MY_API_URL' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

App.js:34 Cross-Origin Read Blocking (CORB) blocked cross-origin response MY_API_URL with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.
Enter fullscreen mode Exit fullscreen mode

After doing some quick research, I've realized I needed to configure my API Server according to the CORS Policy.

First, I've added added mode: 'cors' to my fetch request:

fetch('MY_API_URL', {
  mode: 'cors'
})
.then(function (response) {
  console.log(response);
  return response.json();
})
.then(...);
Enter fullscreen mode Exit fullscreen mode

It was alone no use to my problem. Then, I've added my cors middleware to my API server with only two lines actually.

After installing cors package with:

yarn add cors
Enter fullscreen mode Exit fullscreen mode

I just added these lines to my code:

// import `cors` package
const cors = require('cors');

// use middleware
app.use(cors());
Enter fullscreen mode Exit fullscreen mode

And after running with these configurations, my problem was solved, for now.

3. How to serve Express API Server as HTTPS?

To deploy, I moved my project to my VPS and redirected my my_api_url domain to this VPS. In that way I've put a small layer of abstraction to my server IP, Also, I wouldn't need to type my IP everywhere instead I could use my own domain with fancy subdomains like api.omergulen.com.

In this step, I first tried to deploy it without certification on HTTP.

[blocked] The page at 'https://my_web_app'  was loaded over HTTPS but ran insecure content from 'http://my_api_url': this content should also be loaded over HTTPS.
Enter fullscreen mode Exit fullscreen mode

Yet, my webserver was being server on Firebase Hosting and it was served as https, sending a request from HTTPS to HTTP is called Mixed Content. And it is not allowed to.

So, I just put s at the beginning of the URL :))

https://my_api_url as you can guess, it didn't work either.

GET https://my_api_url net::ERR_SSL_PROTOCOL_ERROR
Enter fullscreen mode Exit fullscreen mode

Then, after doing focused research I've realized that I needed to create a certificate with a Certificate Authority. Many Certificate Authorities were paid but not Let's Encrypt.

Let’s Encrypt is a free, automated, and open Certificate Authority.

If you have shell access to your server, it suggests you use certbot.

In the certbot website, I chose:

My HTTP website is running None of the above on Ubuntu 16.04 (xenial) which was fitting to my case.

Before starting they want you to be sure to have:

  • comfort with the command line
  • and an HTTP website (API Server in my case)
  • which is online
  • and serving on HTTP port (80)
  • which is hosted on a server
  • which you can access via SSH
  • with the ability to sudo

Then just apply the steps on the below:

1. SSH into the server

SSH into the server running your HTTP website as a user with sudo privileges.

2. Add Certbot PPA

You'll need to add the Certbot PPA to your list of repositories. To do so, run the following commands on the command line on the machine:

sudo apt-get update &&
sudo apt-get install software-properties-common &&
sudo add-apt-repository universe &&
sudo add-apt-repository ppa:certbot/certbot &&
sudo apt-get update
Enter fullscreen mode Exit fullscreen mode

3. Install Certbot

Run this command on the command line on the machine to install Certbot.

sudo apt-get install certbot
Enter fullscreen mode Exit fullscreen mode

4. Choose how you'd like to run Certbot

Are you ok with temporarily stopping your website?

Yes, my web server is not currently running on this machine.

Stop your web server, then run this command to get a certificate. Certbot will temporarily spin up a webserver on your machine.

sudo certbot certonly --standalone
Enter fullscreen mode Exit fullscreen mode

No, I need to keep my webserver running.

If you have a web server that's already using port 80 and don't want to stop it while Certbot runs, run this command and follow the instructions in the terminal.

sudo certbot certonly --webroot
Enter fullscreen mode Exit fullscreen mode

In this step, you need to insert your domain into the terminal such as dev.to. After that it will check your web server and look for specific files which it will create and in case of success it should print out like that:

Performing the following challenges:
http-01 challenge for my_api_url
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/my_api_url/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/my_api_url/privkey.pem
   Your cert will expire on 2020-04-01. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
Enter fullscreen mode Exit fullscreen mode

Important Note:
To use the webroot plugin, your server must be configured to serve files from hidden directories. If /.well-known is treated specially by your webserver configuration, you might need to modify the configuration to ensure that files inside /.well-known/acme-challenge are served by the webserver.

4. Installing signed certificate to Express API Server

You'll need to install your new certificate in the configuration file for your API Server.

First, you need to install and import some modules:

yarn add https
Enter fullscreen mode Exit fullscreen mode
// import packages
const https = require('https');
const fs = require('fs');

// serve the API with signed certificate on 443 (SSL/HTTPS) port
const httpsServer = https.createServer({
  key: fs.readFileSync('/etc/letsencrypt/live/my_api_url/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/my_api_url/fullchain.pem'),
}, app);

httpsServer.listen(443, () => {
    console.log('HTTPS Server running on port 443');
});
Enter fullscreen mode Exit fullscreen mode

If you also want to maintain HTTP requests among the HTTPS requests, you can add the following lines, too:

const http = require('http');

// serve the API on 80 (HTTP) port
const httpServer = http.createServer(app);

httpServer.listen(80, () => {
    console.log('HTTP Server running on port 80');
});
Enter fullscreen mode Exit fullscreen mode

In the end your final API Server code will be something like that:

// import required packages
const express = require('express');
const cors = require('cors');

const https = require('https');
const http = require('http');

const fs = require('fs');


const app = express();
app.use(cors());

// create new express app and save it as "app"
const app = express();
app.use(cors());

// create a route for the app
app.get('/', (req, res) => {
  res.send('Hello dev.to!');
});

// another route
app.get('/omergulen', (req, res) => {
  res.send('Hello Omer! Welcome to dev.to!');
});

// Listen both http & https ports
const httpServer = http.createServer(app);
const httpsServer = https.createServer({
  key: fs.readFileSync('/etc/letsencrypt/live/my_api_url/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/my_api_url/fullchain.pem'),
}, app);

httpServer.listen(80, () => {
    console.log('HTTP Server running on port 80');
});

httpsServer.listen(443, () => {
    console.log('HTTPS Server running on port 443');
});
Enter fullscreen mode Exit fullscreen mode

5. Automatic Renewal and Test of the Certificate

The Certbot packages on your system come with a cron job or systemd timer that will renew your certificates automatically before they expire. You will not need to run Certbot again, unless you change your configuration. You can test automatic renewal for your certificates by running this command:

sudo certbot renew --dry-run
Enter fullscreen mode Exit fullscreen mode

The command to renew certbot is installed in one of the following locations:

/etc/crontab/
/etc/cron.*/*
systemctl list-timers
Enter fullscreen mode Exit fullscreen mode

If you needed to stop your webserver to run Certbot, you'll want to edit the built-in command to add the --pre-hook and --post-hook flags to stop and start your web server automatically. For example, if your webserver is HAProxy, add the following to the certbot renew command:

--pre-hook "service haproxy stop" --post-hook "service haproxy start"
Enter fullscreen mode Exit fullscreen mode

More information is available in the Certbot documentation on renewing certificates.

Confirm that Certbot worked

To confirm that your site is set up properly, visit https://yourwebsite.com/ in your browser and look for the lock icon in the URL bar. If you want to check that you have the top-of-the-line installation, you can head to https://www.ssllabs.com/ssltest/.

Well done! You have come to the end of this long tutorial.

After applying these steps you can finally go to your API Server URL and you should be seeing Hello dev.to!.

Thanks for reading

I hope this tutorial was helpful enough.

You can check my last article here:

Feel free to reach out to me at omrglen@gmail.com.

I’m opened to suggestions & requests for future articles, cya 😃

Happy New Year! 🥳🥳🥳

Discussion (19)

Collapse
radulle profile image
Nikola Radulaški

It is best practice to put Node behind Nginx or Apache for several reasons:

  1. Offloading encryption
  2. Offloading Node when serving static content
  3. Handling errors when Node crashed or is restarting
  4. Load balancing
  5. Cache control ...
Collapse
omergulen profile image
Ömer Gülen Author

Thanks!

Collapse
rahmatalimalik5 profile image
RahmatAliMalik5

Well in my experience, it is much better to run Node server behind the defense line of Apache or Nginx server.

Both provides easy way of handling multiple web apps of different kinds with a most trusted security around the world ;)...

Collapse
omergulen profile image
Ömer Gülen Author

It's probably a better way to do, yet tutorial is about serving directly on Node with a certificate. Thanks :)

Collapse
0xdanny profile image
Daniel Etuk

Followed every single step. Great article.

Collapse
omergulen profile image
Ömer Gülen Author

Thanks! I'm glad.

Collapse
naskkotech profile image
Olohundare Nasirudeen

Why don't use nginx instead ?

Thread Thread
naskkotech profile image
Olohundare Nasirudeen

or is there any advantage of this over nginx ?

Collapse
sheburdos profile image
Andrey Shipilov

Why not to wrap components into containers (docker), start them as a stack and add one more component - proxy server based on smth like Caddy, that will give you pretty simple and smooth https with built in certificates from let's encrypt?

Collapse
omergulen profile image
Ömer Gülen Author • Edited

Maybe it's simpler or not. I am not experienced on that, please do write a tutorial about it so I can use it in the future, too. Thanks!

Collapse
spock123 profile image
Lars Rye Jeppesen

Personally I would avoid handling certs in Node, if possible.. that stuff should be handled by the webserver forwarding traffic to your Node application.

Collapse
omergulen profile image
Ömer Gülen Author

Can you give further details about your reasons, please? I would like to learn more about the reason behind it.

Collapse
spock123 profile image
Lars Rye Jeppesen

Hi, sorry for my late reply.
So there are a lot of reasons for taking out certificate handling from your application.

I could list many of the reasons here, but it's easier for me to refer to this article which explains lots of the issues: medium.com/intrinsic/why-should-i-...

tldr; :

  • use a reverse proxy to send requests to your Node application
  • let the proxy handle certificates
  • this scales much better (think multiple servers)
  • performance is better (check benchmarks, it's remarkable)
  • enables easier http->https handling
Thread Thread
omergulen profile image
Ömer Gülen Author

Thanks for your further explanation, I will check them out!

Collapse
awakanto profile image
awakanto

Hi Omer, very great article and guide - very generous of you. My only little issue with this is the oversight from your part to suggest, as pointed out in the comments, that there is another evidently better method of handling ssl certificates in a node.js setup.

I was about to delve in and start searching for the location of my private keys and certificate in my vps (for it already has an installed and paid for certificate) till I decided to check the comments here and realized that indeed there is a better way than exposing my private keys in my app and its many third party packages.

Please suggest to future readers, preferably at the top of the article, that there is another method of implementing this and it is recommended for performance, security and scalability.

Thank you for your effort and great article.

Collapse
prashantnirgun profile image
Prashant Nirgun

hey article is in depth, really a good content. I need to use http2 with SSL can you guide us.

Collapse
fischgeek profile image
fischgeek

Thanks! Though, it looks like you've added this bit twice causing an error:

const app = express();
app.use(cors());
Collapse
rdfrutuoso profile image
rd-frutuoso

Thank you very much for the article, it helped me a lot!

Collapse
mohammadp007 profile image
mohammadp007

I'm having problem accessing my app with https, I'm getting ERR_SSL_PROTOCOL_ERROR.
could you please help me fix it