DEV Community

Cover image for I built SSR in NGINX using njs. Here’s what I learned
Damian Kajzer
Damian Kajzer

Posted on

I built SSR in NGINX using njs. Here’s what I learned

I built a minimal SSR runtime directly inside NGINX — and ended up using it in production.

For some time, I’ve been using NGINX with njs for things like CSP nonce generation and request-level logic.

This started as an extension of something I was already using in production (CSP handling in NGINX).

At some point I asked myself a simple question:

can this go a bit further?

This started as a small experiment — but it ended up running in production on my personal website.


What I built

👉 https://github.com/kraftdorian/nginx-njs-ssr-starter

A minimal SSR-like runtime using:

  • NGINX
  • njs (JavaScript inside NGINX)
  • simple component rendering patterns

No Node.js. No Next.js. Just NGINX.

The runtime is intentionally small and explicit — closer to a rendering utility than a framework.


How it works (high-level)

Instead of:

Client → Node SSR → HTML
Enter fullscreen mode Exit fullscreen mode

I explored:

Client → NGINX (njs) → HTML
Enter fullscreen mode Exit fullscreen mode

The idea is simple:

  • execute JavaScript during request handling (via njs)
  • generate HTML on the fly
  • return it directly from NGINX

In practice, this is a string-first rendering model — HTML is built directly as strings using small helper functions, without a virtual DOM or template engine.


What I learned

1. I underestimated how far NGINX can go

Before this, I treated NGINX mainly as:

  • a reverse proxy
  • a place for headers and routing

Working with njs changed that for me.

I learned that it can also:

  • execute request-level logic
  • generate dynamic responses
  • act as a very lightweight rendering layer

It’s constrained — but that constraint is actually part of the value.


2. SSR became much simpler once I stripped it down

While building this, I kept coming back to a simple realization:

SSR is just returning HTML from the server.

Everything else is layered on top.

This project helped me separate:

  • what SSR actually is
  • how we usually implement it

That distinction alone made the whole topic much clearer for me.


3. Constraints shape the system more than features

This project is intentionally limited:

  • synchronous rendering only
  • no external I/O
  • no hidden state
  • deterministic output

At first this felt restrictive.

In practice, it made the system:

  • easier to reason about
  • easier to debug
  • more predictable

I learned that removing capabilities can improve clarity more than adding abstractions.


4. The trade-offs are very different from typical SSR setups

What I gained:

  • no Node.js runtime
  • very fast execution path
  • minimal moving parts

What I lost:

  • full JavaScript runtime
  • ecosystem and tooling
  • familiar debugging patterns

This shifted my thinking from “application-level design” to something closer to “infra-level design”.


5. Rendering inside the infrastructure layer changes how concerns fit together

One thing that stood out was how naturally this integrates with:

  • CSP
  • headers
  • request validation

Because everything happens inside NGINX, these concerns don’t need to be split across layers.

That made the system feel more cohesive than I expected.


6. I learned how to coordinate between different types of AI tools

An unexpected part of this project was the development workflow itself.

I ended up coordinating between:

  • a text-oriented model (ChatGPT) — for reasoning, structure, and constraints
  • a code-oriented agent (Codex) — for implementation

The runtime itself was implemented with Codex, but always under explicit architectural constraints and iteration from my side.

In practice, this meant:

  • defining the shape of the system first
  • using Codex to fill in implementation details
  • validating everything against real runtime behavior

This separation turned out to be much more effective than trying to generate everything in one step.


When this approach makes sense (from my perspective)

I would consider it for:

  • small or mostly static sites
  • simple rendering layers
  • cases where control over HTML and security matters
  • infra-level experiments

I wouldn’t use it for:

  • complex frontend applications
  • heavy interactivity
  • teams relying on a large ecosystem

Setup (very simple)

If you’ve configured NGINX before, this should feel familiar.

This is intentionally close to a production-like setup.

Basic steps:

  1. Install NGINX with njs
  2. Clone the repository
  3. Load the configuration
  4. Run

Full instructions:
👉 https://github.com/kraftdorian/nginx-njs-ssr-starter


Final thoughts

This started as a small extension of something I was already using (CSP handling in NGINX).

It turned into a useful way to better understand:

  • how far rendering can be pushed into infrastructure
  • how constraints shape system design
  • what we actually need from SSR in practice

Building it — and running it in production on my own site — made those trade-offs much more concrete.

It also changed how I think about where frontend actually ends — and where infrastructure begins.

Sometimes removing layers teaches more than adding new ones.

Top comments (0)