A hand-welded hydraulic press in a metal fabrication shop, with the words 'Build step' crossed out overlaid on the image

Why Server-Rendered Templates Don’t Need a Build Step

The current stack has no build step. Not Deno, not Fresh, not anything purpose-built for this, plain Node, Express, and EJS. Deploying is push source, restart the process. There’s no npm run build, no bundler, no compiled output sitting between the code being edited and the code actually running in production.

That’s not a novel setup. It’s closer to how the web worked before build tooling became the default assumption, and it still works today on an entirely ordinary Node process, for a content-driven site with real traffic.

TL;DR

  • A build step is a set of code transformations, compiling, minifying, bundling, code-splitting, that happens before a request ever reaches a user. The real design question isn’t whether that transformation happens, it’s whether it happens ahead of time in a separate CI/CD step, or just-in-time when the request comes in.
  • EJS templates compile to functions the first time they render, cached afterward, entirely at request time. No separate compile phase, no bundler, no dependency graph to walk before deploy.
  • Deploying means restarting a process, not shipping the output of a build pipeline. That removes an entire category of “did the build actually pick up my change” debugging, because there’s no intermediate artifact that can go stale.
  • A named practice for the same underlying idea, “manual ’till it hurts”, has existed in the indie web community for years, and Deno’s team made a related case in 2023. Both are worth reading directly.
  • This isn’t a claim that build steps are never worth it. Large client-heavy apps with genuine code-splitting needs, heavy asset pipelines, or a team that wants TypeScript enforced as a CI gate all have real reasons to keep one.

What a build step actually does

A build step is any set of code transformations, compiling, minifying, bundling, code-splitting, applied before a request reaches a user. Compiling turns JSX, TypeScript, or other non-browser syntax into plain JavaScript. Minifying strips and renames code to ship fewer bytes. Bundling walks a full dependency graph and packages what an entry point needs into one or more files. Code-splitting chunks that bundle so a page only loads what it actually uses. Each step solves a real problem for a specific kind of application, one built around a component tree and a client-side runtime that needs matching code shipped to the browser.

None of that is required to serve HTML. The actual design question is whether those transformations happen once, ahead of time, in a separate pipeline, or on request, cheaply enough that doing it every time doesn’t matter.

Where EJS actually sits

EJS templates compile to JavaScript functions, but that compilation happens the first time a template renders, gets cached in memory, and never touches a separate build phase. A request comes in, the template renders against real data, HTML goes out. There’s no JSX or TypeScript transform pass, no dependency graph to walk, no bundler deciding what ships to a browser, because there’s no client bundle being produced at all.

Deploying reflects that directly. Pushing new code means restarting the process, not running a compile pipeline first and shipping its output. That’s a meaningful part of what a build step’s absence actually buys: not just faster iteration, but the removal of an entire category of debugging, tracking down why a deployed build didn’t reflect a recent change, that doesn’t exist when there’s no intermediate artifact to go stale in the first place.

Being precise about TypeScript

Worth stating plainly rather than glossing over: the current stack runs no TypeScript at all. It did during an earlier Next.js-based version of this same site and was dropped as part of a later migration, a real, related decision, not an incidental detail. That’s not a claim that TypeScript requires a bundler in every context, TypeScript can and does run in just-in-time setups elsewhere. It’s an honest account of what this particular stack does today: no compile step, no type-check step, plain JavaScript rendered at request time.

Prior art

Deno’s team published a piece making a related case in March 2023, using Next.js’s own blog-starter template as a worked example of build complexity, compiling, minifying, bundling, code-splitting, and Fresh, a Deno framework that renders every page just-in-time with no bundling or transpiling, as their answer. Their framing of the real question as ahead-of-time versus just-in-time transformation is a clean way to state the underlying principle, and it’s worth reading directly rather than taking secondhand. It’s cited here as prior art, not as the source of this stack’s setup: EJS on Node reaches the same just-in-time category through server-side templating, arrived at independently, on a runtime Deno’s piece wasn’t written about.

A separate, older practice exists in the indie web community for the same underlying instinct: manual ’till it hurts. Do something by hand first, introduce automation only once doing it manually genuinely becomes unworkable, and notice how often that second step never actually arrives. Jeremy Keith has written about running several real, current production sites this way, a podcast site, a conference site, a support page, and made the practical case plainly: revisiting a buildless project a year or two later means no dependency updates to chase, no vulnerabilities to patch, no build tooling standing between an edit and seeing it live.

When a build step is still the right call

To be direct about the boundary: a large client-heavy application that genuinely needs code-splitting to keep initial load reasonable, a heavy image or asset optimization pipeline, or a team that specifically wants TypeScript’s type-checking enforced as a CI gate all have real, legitimate reasons to keep a build step. None of that describes a content-driven site rendering server-side templates against small, scattered interactivity, which is the actual shape of site this argument applies to.

Frequently asked questions

Does dropping the build step mean giving up TypeScript entirely?

Not as a general rule, just as a description of this particular stack. TypeScript can run in just-in-time setups elsewhere; this stack specifically dropped it alongside the rest of the build tooling as a related but separate decision.

Isn’t EJS technically compiling templates too?

Yes, and that’s exactly the point rather than an exception to it. EJS compiles templates to functions the first time they render, cached afterward, entirely at request time. That’s the same just-in-time category as any framework built around per-request rendering, just reached through server-side templating instead of a runtime-level feature.

Does this argument only apply to small sites?

It applies to a specific shape of site, content-driven, server-rendered, with small and scattered interactivity, not to size specifically. A large content site with that shape still doesn’t need a build step; a small app with genuinely complex client state might still want one.

Related reading

Two things already covered in this series explain why there’s so little left for a build step to do here in the first place: the case for Alpine.js over React, which removes the client bundle a build step would otherwise need to produce, and why React SSR blocks Node’s event loop, which covers the hydration payload EJS never has to generate at all.

RSS Feed Newsletter
Contact us

Latest Blog Posts