@mary/dromi #
write Deno web servers with Vite.
deno add jsr:@mary/dromi npm:@deno/vite-plugin npm:vite
usage #
setting up Vite #
add the plugin to your Vite config, optionally configuring deno() first so it can resolve your Deno imports:
// vite.config.ts
import { defineConfig } from 'vite';
import deno from '@deno/vite-plugin';
import { dromi } from '@mary/dromi';
export default defineConfig({
plugins: [
deno(),
dromi({ entry: './server.ts' }),
],
});
then add these tasks to your deno.json:
// deno.json
{
"tasks": {
"dev": "vite --configLoader native",
"build": "vite build --configLoader native",
"start": "deno serve -A dist/server/index.mjs"
}
}
handling requests #
export an object with a fetch method, just as you would for deno serve:
// server.ts
export default {
fetch(request) {
const url = new URL(request.url);
switch (url.pathname) {
case '/': {
return new Response(`hello from Deno!`);
}
case '/api/version': {
return Response.json({ deno: Deno.version.deno });
}
}
return new Response(`not found`, { status: 404 });
},
} satisfies Deno.ServeDefaultExport;
building for production #
deno task build
deno task start
the build outputs static assets to dist/client/ and the server to dist/server/index.mjs.
environment variables #
in development, all .env variables are available in Deno.env, following
Vite's env file rules. for production, load them at runtime:
deno serve -A --env-file=.env --env-file=.env.production dist/server/index.mjs
later --env-file arguments take precedence; existing environment variables override the files.
type checking #
there are two approaches you could take to get Deno to type-check both server-side and client-side code correctly, either by using tsconfig references or Deno's workspaces functionality:
tsconfig references #
Deno can read tsconfig.json project references.
// tsconfig.json
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.server.json" }]
}
// tsconfig.app.json
{
"compilerOptions": {
"lib": ["dom", "esnext"],
"types": ["vite/client"]
},
"include": ["src"]
}
// tsconfig.server.json
{
"compilerOptions": {
"lib": ["deno.window"],
"types": ["vite/client"]
},
"include": ["server.ts"]
}
be sure to omit compilerOptions from deno.json as even an empty object makes Deno skip tsconfig.json.
workspace member #
put client code in a separate folder with a deno.json. list the folder in workspace so Deno reads its
config:
// deno.json
{
"workspace": ["./client"],
"compilerOptions": {
"types": ["vite/client"]
}
}
// client/deno.json
{
"compilerOptions": {
"lib": ["dom", "esnext"],
"types": ["vite/client"]
}
}
static assets and client apps #
files in Vite's public/ directory are served as-is. to add a client app, put an index.html in the Vite
root and configure its client plugins.
matching assets are served before your handler; unmatched requests go to your handler. /about/ serves
/about/index.html.
running the server before assets #
use runServerFirst when your handler needs to see a request even if an asset exists at that path:
dromi({
entry: './server.ts',
runServerFirst: ['/api/*', '!/api/public/*'],
});
here, /api/* goes straight to the server, while /api/public/* is served from assets first. set
runServerFirst: true to send every request to the server first.
fetching assets from a handler #
fetchAsset() serves an asset from within your handler, useful for falling back to assets or modifying an
asset response:
// server.ts, with runServerFirst: true
import { fetchAsset } from '@mary/dromi/server';
export default {
async fetch(request) {
const response = await fetchAsset(request);
const headers = new Headers(response.headers);
headers.set('x-served-by', 'my-server');
return new Response(response.body, {
status: response.status,
headers,
});
},
} satisfies Deno.ServeDefaultExport;
when no asset matches, it uses notFoundHandling, which defaults to a
plain 404.
handling unmatched requests #
use notFoundHandling to serve a client app or custom 404 page when no asset matches.
SPA fallback #
with 'single-page-application', page navigations that match no asset are served /index.html without
running your handler:
dromi({
entry: './server.ts',
notFoundHandling: 'single-page-application',
});
page navigations have Sec-Fetch-Mode: navigate. other requests, such as fetch('/api/users'), still go to
your handler. for unmatched GET and HEAD requests, fetchAsset() serves /index.html.
requests matching runServerFirst always go to your handler.
custom 404 pages #
with '404-page', fetchAsset() serves the nearest 404.html with a 404 status for unmatched GET and HEAD
requests. /blog/missing looks for /blog/404.html, then /404.html.
dromi({
entry: './server.ts',
notFoundHandling: '404-page',
});
unmatched requests still go to your handler. call fetchAsset() after your routes to serve the 404 page:
// server.ts
import { fetchAsset } from '@mary/dromi/server';
export default {
fetch(request) {
// ...your routes
return fetchAsset(request);
},
} satisfies Deno.ServeDefaultExport;
server-side rendering #
add client scripts to environments.client.build.rolldownOptions.input:
// vite.config.ts
export default defineConfig({
environments: {
client: {
build: {
rolldownOptions: {
input: { main: './client/main.ts' },
},
},
},
},
plugins: [
deno(),
dromi({ entry: './server.ts' }),
],
});
use resolveClientEntry('main') to load that entry in server-rendered HTML:
// server.ts
import { resolveClientEntry } from '@mary/dromi/server';
export default {
fetch(request) {
const { entry, js, css } = resolveClientEntry('main');
const html = `<!doctype html>
<html>
<head>
${css.map((href) => `<link rel="stylesheet" href="${href}">`).join('')}
${js.map((href) => `<link rel="modulepreload" href="${href}">`).join('')}
<script type="module" src="${entry}"></script>
</head>
<body>...</body>
</html>`;
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
},
} satisfies Deno.ServeDefaultExport;
in development, Vite loads CSS through JavaScript, so pages may briefly render unstyled.
examples #
- server-only — public assets, Deno imports, module-relative files, and 404 pages
- client-app — a multi-page Vite client with server API routes and SPA fallback
- server-first — route exclusions and
fetchAsset() - server-rendering — server-rendered pages with
resolveClientEntry() - external-deps — native Deno imports without
@deno/vite-plugin