Building a local https server with Expressjs

Building a local https server with Expressjs

As part of the tools which works with Single SPA Framework we have one called import-map-overrides which allow us override the import maps values. By overriding an specific "micro front end" with a local environment version we are able to work with the deployed environment and improve our experience as a full stack engineer.

However, working with the deployed environment "is not all roses", specially if it is over HTTPS... and one of those problems is "Same Origin Policy"

The Same-origin policy

"The same-origin policy is a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin."

For this URL https://maxmartinez.dev/about-me we can have the following origin comparisons examples:

To tackle this situation we are going to setup an https server for a local development.

Setting up the project

Firstly, we have to create our nodejs project:

npm init --yes # this will trigger automatically populated initialization with default values

Then, install the project's dependencies which are necessary to work with typescript and expressjs.

npm install express office-addin-dev-certs cors 
# Dev dependencies
npm install --save-dev @types/express @types/node @types/cors typescript ts-node ts-node-dev

Special mention for "office-addin-devs-certs" which allows us to manage certificates for development server using localhost.

Project structure

Create the following structure in your local project folder:

.
├── src
│   ├── app.ts
│   └── www
│       ├── api.router.ts
│       ├── http.server.ts
│       ├── https.server.ts
│       └── server.interface.ts
└── tsconfig.json

Typescript configurations

For setup typescript properly, add the following settings to the tsconfig.json file:

{
  "compilerOptions": {
    "target": "es2017",
    "outDir": "./dist",
    "rootDir": "src",
    "moduleResolution": "node",
    "module": "commonjs",
    "declaration": true,
    "inlineSourceMap": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "types": ["node"],
    "typeRoots": ["node_modules/@types"]
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules/**"],
}

The web server layer

Server interface

We are going to define the IServer interface contract which ensure that the HTTP and HTTPS servers implement the same behavior. Add the following interface into the server.interface.ts file:

export interface IServer {
  port: number;
  bootstrap: () => Promise<void>;
  listen: () => void;
}

Let's explain the IServer interface:

port: Define port number which the server use to listens the connections.

bootstrap: The bootstrap method allows to implement what "staffs" the server needs to do before start. For example: Load routes.

listen: The listen method allows to implement how the frameworks start listening the connections.

HTTP Server

Add the HttpServer class into the http.server.ts file to handle the Express web server configuration over http protocol:

import express from "express";
import { apiRouter } from "./api.router";
import { IServer } from "./server.interface";

export class HttpServer implements IServer {
  port: number;
  protected framework: express.Application;
  constructor(port: number) {
    this.port = port;
    this.framework = express();
  }

  async bootstrap(): Promise<void> {
    // Load Express Middlewares
    this.framework.use(this.loadMiddlewares());

    // Routers
    this.framework.use(apiRouter());
  }

  async listen(): Promise<void> {
    this.framework.listen(this.port, () => {
      console.log(`[HTTP] Server started at port: ${this.port}`);
    });
  }

  protected loadMiddlewares() {
    return [express.json()];
  }
}

bootstrap: The "bootstrap" method is setting the express json middleware which parses incoming requests with JSON payloads. And also, this method is loading the API Routes.

listen: The "listen" method encapsulate the logic related to how the "ExpressJs Framework" needs to be configured to listening for connections.

loadMiddlewares: Load the builtin express middlewares. In this case, we only have "express.json()" midleware.

HTTPS Server

Basically the HttpsServer class extends from HttpServer class and override the listen method to add the https feature.

Add the HttpsServer class into the https.server.ts file to handle the Express web server configuration over https protocol:

import https from "https";
import cors from "cors";
import { HttpServer } from "./http.server";
import devCerts = require("office-addin-dev-certs");

export class HttpsServer extends HttpServer {
  async listen() {
    const options = await devCerts.getHttpsServerOptions();
    const server = https.createServer(options, this.framework);
    server.listen(this.port, () => {
      console.log(`[HTTPS] Cors enabled: *`);
      console.log(`[HTTPS] Server started at port: ${this.port}`);
    });
  }

  protected loadMiddlewares() {
    return [...super.loadMiddlewares(), cors()];
  }
}

listen: By using the node:https module we need to provide two parameters:

  • The localhost certificate options (from "office-addin-dev-certs")

  • Express instance (this.framework) as "Request Listener"

loadMiddlewares: We override the super class method to keep the HTTP server middlewares and add the "cors" middleware only for HTTPS scope.

API Router

To define how our web server handles client requests we have to implement the ExpressJs Router. So, add the following functions into the api.router.ts file:

import {
  Router,
  Request as ExpressRequest,
  Response as ExpressResponse,
  NextFunction as ExpressNextFunction,
} from "express";

const router = Router();

export const apiRouter = () => {
  router.get(
    "/health-check",
    (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => {
      res.status(200).json({ status: "OK" });
    }
  );

  return router;
};

\we only define the "/health-check" route. You can have whatever you need.*

App

The app acts as the central point for managing the running process. So, add the App class into the app.ts file:

import { HttpServer } from "./www/http.server";
import { HttpsServer } from "./www/https.server";
import { IServer } from "./www/server.interface";

export class App {
  constructor(private server: IServer) {}
  async bootstrap(): Promise<void> {
    // framework bootstrapping
    await this.server.bootstrap();
  }

  async run(): Promise<void> {
    this.server.listen();
  }
}

const makeServerInstance = (port: number): IServer => {
  return process.env.ENABLE_HTTPS === "true"
    ? new HttpsServer(port)
    : new HttpServer(port);
};

async function start() {
  const port = 9001;
  const server = makeServerInstance(port);
  const app = new App(server);
  await app.bootstrap();
  await app.run();
}

start();

bootstrap: The "bootstrap" method initializes the web server.

run: The "run" method encapsulates the logic to put the server running.

makeServerInstance: A "factory" function which make and return the http or https server instance.

start: The "start" function set the server port, injected the Application Dependencies (web server), initialize the application and run the server.

Package json

To run our project we have to modify the scripts section of the package.json file and ad the necessary scripts to build and run the http or https server.

...
"scripts": {
    "build": "tsc --build",
    "start": "ts-node-dev --respawn ./src/app.ts",
    "start:https": "ENABLE_HTTPS=true npm start"
  },
...

build: Build the typescript project and their dependencies.

start: Start express server over HTTP protocol

start:https: Start express server over HTTPS protocol. By passing the "ENABLE_HTTPS" environment variable equal true we tell to the App that we wanted to run a HTTPS server.

Testing

HTTP Server

In your terminal execute the followed command to start an instance of http server:

npm run start

HTTP CURL request:

curl -I http://localhost:9001/health-check
#output
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 15
ETag: W/"f-v/Y1JusChTxrQUzPtNAKycooOTA"
Date: Fri, 12 Apr 2024 04:48:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5

HTTPS Server

To start an https server, execute in your terminal execute:

npm run start:https

HTTPS CURL request:

curl --verbose https://localhost:9001/health-check
#output
*   Trying [::1]:9001...
* Connected to localhost (::1) port 9001
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=127.0.0.1
*  start date: Apr  8 05:44:00 2024 GMT
*  expire date: May  8 05:44:00 2024 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: CN=Developer CA for Microsoft Office Add-ins; C=US; ST=WA; L=Redmond; O=Developer CA for Microsoft Office Add-ins
*  SSL certificate verify ok.
* using HTTP/1.1
> GET /health-check HTTP/1.1
> Host: localhost:9001
> User-Agent: curl/8.4.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Access-Control-Allow-Origin: *
< Content-Type: application/json; charset=utf-8
< Content-Length: 15
< ETag: W/"f-v/Y1JusChTxrQUzPtNAKycooOTA"
< Date: Fri, 12 Apr 2024 05:06:11 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< 
* Connection #0 to host localhost left intact

We can notice that the issuer of the certificate is "Developer CA for Microsoft Office Add-ins" and also, we got a new header related the "cors" configuration: "Access-Control-Allow-Origin: *"

Now that you’ve read this post, you know exactly how implement an http & https server and what useful it is when you need to integrate your deployed environment with your local env.

See you in the next article.

Get the source files from my Github repository.

Did you find this article valuable?

Support Max Martínez Cartagena by becoming a sponsor. Any amount is appreciated!