Setup with Node
Prerequisites
- Node.js v18+
Create a new project
mkdir my-app && cd my-app
npm init -y
Install emroute
npm add @emkodev/emroute
emroute ships compiled JavaScript alongside TypeScript source. Node uses the compiled .js files automatically — no extra loaders or flags needed.
Configure TypeScript
emroute components use DOM APIs (custom elements, URLPattern), so your tsconfig.json needs DOM types. Add "DOM" and "DOM.Iterable" to your existing lib array — don't replace the whole file or you'll drop any strictness flags your editor/build already relies on:
{
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "nodenext",
"strict": true,
"skipLibCheck": true,
"noEmit": true
}
}
emroute is compatible with strict TypeScript options including noUncheckedIndexedAccess and noImplicitOverride — keep them enabled if your existing config has them.
First route
Make a routes/ directory and add an HTML page:
<h1>Hello emroute</h1>
<p>This is my first page.</p>
To use Markdown (
.page.md) instead, you'll need a markdown renderer. See Markdown Renderers — emkoma is built for emroute (handlesrouter-slotand widget fences natively); marked and markdown-it also work with a small adapter.Heads up: the root
index.page.htmlalso acts as the layout for every child route (e.g./about). As soon as you add a second route, this file needs a<router-slot></router-slot>where the child should render — otherwise the child page won't appear. See Nesting.
Write the server
Create server.ts in your project root:
import { createServer } from 'node:http';
import { Emroute } from '@emkodev/emroute/server';
import { UniversalFsRuntime } from '@emkodev/emroute/runtime/universal/fs';
const appRoot = import.meta.dirname!;
const runtime = new UniversalFsRuntime(appRoot);
const emroute = await Emroute.create({
spa: 'none',
title: 'My App',
}, runtime);
createServer(async (req, res) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
const request = new Request(url, { method: req.method });
const response = await emroute.handleRequest(request);
if (!response) {
res.writeHead(404).end('Not Found');
return;
}
res.writeHead(response.status, Object.fromEntries(response.headers));
res.end(await response.text());
}).listen(1420, () => {
console.log('http://localhost:1420/');
});
UniversalFsRuntime uses only node: APIs — no platform-specific dependencies.
Run it
Node needs a TypeScript loader for your own .ts files. Use tsx or Node's built-in type stripping:
# Option 1: tsx (recommended)
npx tsx server.ts
# Option 2: Node 22+ built-in
node --experimental-strip-types server.ts
Note:
--experimental-strip-typesonly works for your own files, notnode_modules. emroute ships compiled JS so this works — but your own server code still needs a loader or the flag.
Verify
curl http://localhost:1420/html/ # → HTML page
curl http://localhost:1420/md/ # → Plain text rendering
Next: Pages