Skip to content

Express Web Server

A small Express 5 blog: a JSON API, server-rendered article pages, and a static frontend. Express comes straight from npm with no fork and no shim in your code.

One src/index.ts serves on all four runtimes with no branch anywhere in it, GJS included. If you already have an Express service, this is what porting it looks like: nothing.

It declares all four runtimes, so pick the one you have:

Terminal
gjsify showcase express-webserver --runtime gjs
gjsify showcase express-webserver --runtime node
gjsify showcase express-webserver --runtime bun
gjsify showcase express-webserver --runtime deno

There is no window. It listens on port 3000, on a GLib main loop under gjs and on the host loop under the other three. Open http://localhost:3000/ in a browser, or hit the API:

Terminal
curl http://localhost:3000/api/posts
# → {"count":…,"posts":[…]}
curl http://localhost:3000/api/posts/hello-gjs
curl http://localhost:3000/api/runtime
# → {"runtime":"GJS","platform":"linux","version":"…","time":"…"} (--runtime gjs)
# → {"runtime":"Node.js","platform":"linux","version":"…","time":"…"} (--runtime node)

Set PORT to listen somewhere else.

/api/runtime is the interesting one. It reports which JavaScript engine is serving the request, from runtimeName in @gjsify/runtime. Run the same source on another runtime and that one field changes. Nothing else does, including the routes, the rendered pages and the JSON.

From showcases/node/express-webserver/ in a checkout you can build both bundles and run them side by side:

Terminal
gjsify run build
gjsify run start # the --app gjs bundle
gjsify run start:node # the --app node bundle

Same src/index.ts, two bundles: dist/index.gjs.js and dist/index.node.mjs. The second is what node, bun and deno all run.

The build target decides which server implementation Express lands on, and this is the one place the four runtimes genuinely diverge.

Under --app node the node: imports stay as they are, so Express gets the host runtime’s own http, stream, events and url. Under --app gjs there are no such builtins, so gjsify build rewrites each import to a GNOME-backed package:

PackageWhat Express needs it for
@gjsify/httphttp.createServer(), Server.listen(), IncomingMessage and ServerResponse
@gjsify/streamreq.pipe(res) and body parsing
@gjsify/eventsEventEmitter, which Express and its middleware lean on throughout
@gjsify/urlURL parsing in the route matcher
libsoup 3.0The TCP listener and HTTP/1.1 parser under @gjsify/http

You install none of them, and you import none of them. How it works walks through the rewrite.

showcases/node/express-webserver/

  • src/index.ts is the whole app: middleware, the JSON API, the server-rendered pages and app.listen().
  • src/data/posts.ts holds the in-memory post fixtures the API and the pages both read.
  • src/public/ is the static frontend, served with express.static().

There is no shell to write here, because a server has no UI. The only per-target file is the bundle.