This is a frameworkless Node.js/Typescript/Express/React stack to serve SSR React apps. It prefers explicit code over magic glue. It cloc
s in at 300 total lines of code.
It's an alternative to opinionated frameworks (Next.js, Remix, Sveltekit, others) which leans on basic tools like esbuild
and express
to assemble an SSR-capable backend. There's no man behind the curtain:
- no forced route file conventions that are difficult/impossible to override
- no hidden build process
- no one-true-way to build middleware, use a queue, or do authentication
- etc.
Instead, all of the plumbing is in plain sight and is fully customizable.
It adapts one useful convention from Remix: the interior structure of a route file. Route files are structured as a pair: a "loader" function that runs on the server, and a "view" function that uses that data to render the React view on both server and client.
- Simple main stack. Just vanilla Node, Express, and React.
- Simple build system. It's
esbuild
,nodemon
, andconcurrently
: all of which are do-one-thing-well tools that are manually glued together. No config-heavy do-everything tools like Vite, Webpack, etc. (Vite and others have a place, but they're not always needed). - No HMR (hot module replacement) or live reload. These add unnecessary complexity. Instead, the server is immediately restarted on changes, and the browser can be manually refreshed to see the changes.
- Live reload could be added simply using something like this it's it really important.
- Same build output format for both dev and production. The
dist/
folder. There's no live-served, uninspectable build output.- If you want to customize prod vs. dev build configuration, that's easy to do by editing the esbuild configuration in
scripts/build*
files.
- If you want to customize prod vs. dev build configuration, that's easy to do by editing the esbuild configuration in
- Tree-shaking natively. Server-side code like database clients won't end up in client-side bundles.
- More manual steps to register loaders and views. See the
src/handlers/registry.ts
file for how this works. This is necessary to avoid a build step that scans for a file structure. - No client-side bundle splitting by route/view. This is nice to have for SEO- or performance-sensitive apps, but not always needed for small apps or apps where the average user session is expected to span multiple page loads.
- Image bundling, WebAssembly bundling, etc. requires additional work. There's typically an esbuild plugin for just about everything, and it's trivial to build your own plugin if not.
- SSR rendering is done using
renderToString
. This doesn't support streaming. Streaming adds complexity and isn't necessary for most use cases.
- Clone this repo.
- Delete the
.git
folder to start with a fresh git history. - Delete the
LICENSE
file and "License" section of the README. - Install dependencies with
npm install
. - Run the local server with
npm run dev
.nodemon
will error the first time you do this; just terminate and re-run the command.
This repo is intentionally kept slim; you'll probably want to add:
- A formatter like
prettier
- Linting with
eslint
- Typechecking command with Typescript's
tsc
- CI/CD jobs for running the above
- React styling library like
emotion
- A static asset bundling plugin (e.g. for images)
To build for deployment:
npm run build
The build is placed into the dist/
directory. This directory is fully self-contained. It can be deployed to a server or copied into a Dockerfile or whatever.
Public domain via The Unlicense. If you find this useful, you can say "thanks" by showing kindness today to someone who doesn't deserve it.