Setup a JS server in a minute

Introduction

Sometimes we need to mess around with a node module or some Javascript-related code. There is a bunch of NPM packages out there that set up a server for you, but today we're going to see how easy and quick it can be to set up one ourselves.

The goal is to render an HTML file, along with a javascript bundle, served by a basic javascript HTTP server.

  • yarn: A package manager. I'm going with yarn because I'm used to it, not because I have any technical reason in its favor. Use npm if you prefer.
  • esbuild: A bundler. You write your javascript code, import in it any packages that have been installed using yarn or npm, then compile (bundle) all of it in an output file that'll be executed by a Javascript runtime (either a browser for the client-side, or Node for the server-side).
  • express: An HTTP server.
  • node: The server-side Javascript runtime that will execute the express server.

The goal is neither to write production-ready code nor to run a secure server.

Setup your JS server, step by step

1. Initialize a directory with yarn

$ mkdir myapp
$ cd myapp

$ yarn init .
# Or if you're in a hurry:
$ yarn init -y .

2. Create a structure for the client-side code

Put this in the index.html file:

<!DOCTYPE html>
  <head>
    <script src="dist/bundle-client.js"></script>
  </head>
  <body>
  </body>
</html>

Notice we're linking not to client.js (the source file) but to dist/bundle-client.js, the output that esbuild will produce.

Next, create a dummy client.js file:

console.log("hello, world")

3. Setup esbuild for bundling client-side code

First, add esbuild to your app:

$ yarn add esbuild

We will be using the ESM syntax instead of CommonJS (for instance, we'll be using the import syntax instead of require). This has to be declared in the manifest file (package.json).

Add "type": "module" to package.json:

{
  "name": "myapp",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "dependencies": {
    "esbuild": "^0.20.1"
  }
}

Now let's add a scripts section to the manifest which will contain CLI commands for bundling the code, and later for running the server. In this section, we'll declare a first build-client command. This command will tell esbuild to:

  • bundle client.js into dist/bundle-client.js,
  • consider client.js to be written in esm format,
  • generate es2022 -complient code (which should be understood by the majority of web browsers nowadays).
{
  "name": "myapp",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "build-client": "esbuild client.js --bundle --format=esm --target=es2022 --outfile=dist/bundle-client.js"
  },
  "dependencies": {
    "esbuild": "^0.20.1"
  }
}

We could tell it to minify the output, generate a sourcemap and whatnot, but I want to stick to our goal of quickly building a minimal server. The documentation is easy to find if you need it, though.

To build the client-side bundle:

$ yarn build-client

5. Create a structure for the server-side code

Add express.js to your app:

$ yarn add express

Next, create a server.js file that instantiates a basic express server. This server will simply listen on localhost port 3000 and serve static files. In our case, the two files we want it to serve are index.html and dist/bundle-server.js.

import express from 'express'

const app = express()
app.use(express.static('./'))
app.listen(3000, '127.0.0.1')

We could set it up with routing logic, logging, error management and whatnot, but again, let's keep it minimal and refer to the documentation if need be.

Warning: again, this is neither optimized nor safe for production. We're just building a sandbox to mess around.

6. Setup esbuild for bundling server-side code

Add a build-server command to the scripts section of your manifest file. This command will tell esbuild to:

  • bundle server.js into dist/bundle-server.js,
  • consider server.js to be written in esm format,
  • generate node20.6 -complient code (adapt it to whatever node version you're running locally) that will be executed by a node engine (--platform=node),
  • not bundle into the output file any dependency (--packages=external), assuming dependencies will be available at runtime (in node_modules for instance).
{
  "name": "myapp",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "build-client": "esbuild client.js --bundle --format=esm --target=es2022 --outfile=dist/bundle-client.js",
    "build-server": "esbuild server.js --bundle --format=esm --platform=node --target=node20.6 --packages=external --outfile=dist/bundle-server.js",
  },
  "dependencies": {
    "esbuild": "^0.20.1",
    "express": "^4.18.3"
  }
}

To build the server-side bundle:

$ yarn build-client

7. Run the server

Let's add some more commands to the manifest file:

  • build: build both client-side and server-side bundles,
  • start: run the express server,
  • s: call both build and run commands at once (for when we're in a hurry..)
{
  "name": "myapp",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "build-client": "esbuild client.js --bundle --format=esm --target=es2022 --outfile=dist/bundle-client.js",
    "build-server": "esbuild server.js --bundle --format=esm --platform=node --target=node20.6 --packages=external --outfile=dist/bundle-server.js",
    "build": "yarn build-client && yarn build-server",
    "start": "node dist/bundle-server.js",
    "s": "yarn build && yarn start"
  },
  "dependencies": {
    "esbuild": "^0.20.1",
    "express": "^4.18.3"
  }
}

You can now execute yarn s and visit http://localhost:3000.

There you go.

Thank you for reading!
Younes SERRAJ