Deno
Deno natively supports the fetch handler pattern, making it a natural fit for Juice. With built-in TypeScript, a secure-by-default permissions model, and Deno KV for globally distributed storage, Deno is a strong choice for both self-hosted and edge deployments via Deno Deploy.
Server Entry
Deno.serve() takes a fetch handler directly -- same pattern as Bun, no adapter needed.
// server.ts
import { createRouter } from '@cmj/juice/runtime';
import manifest from './flight-manifest.json';
const handler = createRouter(manifest, {
root: import.meta.url,
});
Deno.serve({ port: 3000 }, handler);Deno supports Request/Response natively, so the fetch handler is called with zero conversion overhead.
Deploy
Deno Deploy
Deno Deploy is the fastest path to production. It runs your Worker at the edge with zero configuration:
juice build && deployctl deploy --project=my-juice-app dist/server.jsInstall deployctl if you have not already:
deno install -A jsr:@deno/deployctldeno.json
A minimal deno.json for a Juice project:
{
"tasks": {
"dev": "deno run --allow-net --allow-read --allow-env server.ts",
"start": "deno run --allow-net --allow-read --allow-env dist/server.js"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}Permissions
Deno requires explicit permissions for all system access. A Juice app typically needs:
deno run \
--allow-net \
--allow-read \
--allow-env \
server.ts--allow-net-- required to listen on a port and make outbound HTTP requests (data fetching, API calls)--allow-read-- required to read the flight manifest, route modules, and static assets from disk--allow-env-- required to read environment variables
You can scope permissions to specific paths and hosts for tighter security:
deno run \
--allow-net=0.0.0.0:3000,api.example.com \
--allow-read=./dist,./app \
--allow-env=DATABASE_URL,APP_ENV \
server.tsDeno KV
Deno KV is a built-in key-value store that works locally and scales globally on Deno Deploy. Share it via context:
// server.ts
import { createRouter } from '@cmj/juice/runtime';
import { createContextKey, setContext } from '@cmj/juice/runtime';
import manifest from './flight-manifest.json';
export const kvKey = createContextKey<Deno.Kv>('kv');
const kv = await Deno.openKv();
const router = createRouter(manifest, {
root: import.meta.url,
onBeforeRequest: async (req) => {
setContext(req, kvKey, kv);
},
});
Deno.serve({ port: 3000 }, router);Read and write from any route:
// app/routes/api/counter.tsx
import { getContext } from '@cmj/juice/runtime';
import { kvKey } from '../../../server.js';
export async function GET({ request }: { request: Request }) {
const kv = getContext(request, kvKey);
// Atomic read-modify-write
const entry = await kv.get<number>(['counter']);
const current = entry.value ?? 0;
await kv.set(['counter'], current + 1);
return new Response(JSON.stringify({ count: current + 1 }));
}
export async function POST({ request }: { request: Request }) {
const kv = getContext(request, kvKey);
const body = await request.json();
// Store with expiration
await kv.set(['session', body.id], body.data, {
expireIn: 3600_000, // 1 hour in ms
});
return new Response(JSON.stringify({ ok: true }));
}On Deno Deploy, KV is globally distributed -- reads are served from the nearest edge location, writes are strongly consistent.
File System
Deno provides its own file system APIs. These require the --allow-read and --allow-write permissions:
// Reading files
const data = await Deno.readTextFile('./data/config.json');
const bytes = await Deno.readFile('./data/image.png');
// Writing files
await Deno.writeTextFile('./data/output.txt', 'Hello, world!');
await Deno.writeFile('./data/image.png', bytes);File uploads from a server action:
async function handleUpload(formData: FormData) {
'use server';
const file = formData.get('file') as File;
const bytes = new Uint8Array(await file.arrayBuffer());
await Deno.writeFile(`./uploads/${file.name}`, bytes);
return { success: true };
}npm Compatibility
Deno supports npm packages via the npm: specifier. Most Juice dependencies work without changes:
import { drizzle } from 'npm:drizzle-orm';
import { sqliteTable, text, integer } from 'npm:drizzle-orm/sqlite-core';You can also use an import map in deno.json to keep imports clean:
{
"imports": {
"drizzle-orm": "npm:drizzle-orm@^0.36.0",
"drizzle-orm/": "npm:drizzle-orm@^0.36.0/"
}
}With the import map, you use the same import paths as Node.js and Bun:
import { drizzle } from 'drizzle-orm';Environment Variables
Access environment variables with the Deno API:
const dbUrl = Deno.env.get('DATABASE_URL');On Deno Deploy, set environment variables via the dashboard or CLI. For local development, create a .env file and load it:
deno run --allow-env --env .env server.tsThe --env flag tells Deno to load the .env file automatically.