Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nuejs/nue/llms.txt

Use this file to discover all available pages before exploring further.

Nue sites are static by default, but you can add dynamic behavior by placing server routes in @shared/server/. These routes use the same Request / Response web-platform APIs that edge runtimes expose, so the code you write locally runs unchanged on Cloudflare Workers, Deno Deploy, or any other edge environment. Nue loads your routes automatically during development and preview, routing matching requests to your handlers while serving all other traffic as static files.

Where server routes live

Server routes go in the @shared/server/ directory at your project root. Nue’s worker picks up @shared/server/index.js as the entry point:
my-site/
├── @shared/
│   └── server/
│       ├── index.js      ← entry point, register routes here
│       └── data/
│           ├── posts.json
│           └── users.json
├── blog/
│   └── index.md
└── site.yaml
The @shared/ directory is excluded from the static build — its contents never appear in .dist/. You can override the default directory with the server key in site.yaml:
# site.yaml
server:
  dir: "@shared/server"

Registering routes

Routes are registered using the nue-edgeserver package, which provides a small routing layer on top of the standard Request / Response APIs:
// @shared/server/index.js
import { router } from 'nue-edgeserver'

router.get('/api/posts', async (req, env) => {
  const posts = await env.posts.getAll()
  return Response.json(posts)
})

router.post('/api/posts', async (req, env) => {
  const body = await req.json()
  const post = await env.posts.create(body)
  return Response.json(post, { status: 201 })
})

router.get('/api/posts/:id', async (req, env) => {
  const { id } = req.params
  const post = await env.posts.get(id)
  if (!post) return new Response('Not found', { status: 404 })
  return Response.json(post)
})
Each handler receives a standard Request object and an env object populated from JSON files in @shared/server/data/ (see Data models below).

Request and response handling

Route handlers use the standard web-platform Request and Response APIs with no Node.js-specific extensions. Reading request data:
// Query parameters
const url = new URL(req.url)
const page = url.searchParams.get('page') ?? '1'

// JSON body
const body = await req.json()

// Form data
const form = await req.formData()
const email = form.get('email')

// Headers
const token = req.headers.get('authorization')
Returning responses:
// JSON
return Response.json({ ok: true })

// Plain text
return new Response('Hello', { status: 200 })

// Custom headers
return new Response(body, {
  status: 200,
  headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }
})

// Redirect
return Response.redirect('/login', 302)
Do not use Node.js-specific APIs (fs, path, process.env, etc.) inside server routes. These APIs are not available in edge runtimes. Use the env parameter for data access instead.

The worker model

In development and preview, Nue loads @shared/server/index.js inside a worker — a Bun-native environment that mimics edge runtime behavior. The worker:
  1. Imports your index.js and collects all registered routes.
  2. Intercepts incoming requests before the static file handler.
  3. Matches requests by HTTP method and pathname.
  4. Calls your handler and returns the Response directly to the client.
When no route matches, the worker returns null and Nue falls back to serving the corresponding static file from .dist/ (in preview) or from the source tree (in development).

Simulated Cloudflare headers

During development the worker automatically injects Cloudflare-style request headers so you can test geo-based logic without deploying:
cf-ipcountry: FI
cf-ipcity: Helsinki
cf-ipcontinent: EU
cf-iplongitude: 24.95034
cf-iplatitude: 60.18427
cf-region: Uusimaa
cf-timezone: Europe/Helsinki
cf-connecting-ip: 127.0.0.1
Your route handlers can read these headers via req.headers.get('cf-ipcountry') exactly as they would in production.

Hot reload in development

When the development server is running, changes to files inside @shared/server/ trigger a worker reload. The worker clears its route registry, re-imports index.js, and re-registers all routes — all without restarting the dev server.

Data models

JSON files placed in @shared/server/data/ are automatically loaded and passed to your route handlers as the env parameter. Each file becomes a model named after the file (without the .json extension):
@shared/server/data/
├── posts.json
└── users.json
Each model exposes a simple CRUD interface:
// In a route handler:
const posts = await env.posts.getAll()   // → array of all items
const post  = await env.posts.get(id)    // → single item with .update() and .remove()
const newPost = await env.posts.create({ title, body })  // → created item with id and created timestamp
const count = await env.posts.size()     // → number of items
Items are augmented with sequential id and created timestamp fields when loaded. The users model additionally provides login, logout, and authenticate helpers backed by session IDs stored in .nue/sessions.json.
The built-in data models are designed for development prototyping. In production, replace them with real database calls or a KV store backed by your edge provider.

Proxy mode

Proxy mode lets you test your frontend against a live remote backend instead of the local worker. Configure it with the server.url and server.routes keys in site.yaml:
# site.yaml
server:
  url: https://api.example.com
  routes:
    - /api/
    - /auth/
When server.url is set, Nue skips the local worker entirely and forwards any request whose path starts with one of the listed prefixes to the remote URL. The proxy strips your local host and rewrites the destination:
GET http://localhost:8080/api/posts
  → GET https://api.example.com/api/posts
Request headers and body are forwarded verbatim. Responses are passed back to the browser unchanged.
Use proxy mode when your backend team owns a staging environment. Your frontend code stays identical — just swap server.url in site.yaml to point at staging instead of running a local mock.

Server routes and static pages together

Nue resolves each incoming request using the following priority order:
  1. Server route match — if the worker (or proxy) claims the request, its Response is returned.
  2. Static file — if no route matches, Nue looks for a file in .dist/ (preview) or the source tree (development).
  3. 404 page — if .dist/404.html exists, it is served for unmatched non-file requests.
This means you can have a static blog/ section and a dynamic /api/ section in the same project with no configuration beyond registering routes in @shared/server/index.js.
GET /blog/post-1    → serves .dist/blog/post-1/index.html  (static)
GET /api/posts      → handled by router.get('/api/posts')  (dynamic)
GET /missing-page   → serves .dist/404.html                (fallback)

Edge-compatible APIs

Write routes as if they will run in a Cloudflare Worker or Deno Deploy environment. The following globals are available in both development and production:
APINotes
RequestStandard Fetch API Request
ResponseStandard Fetch API Response, including Response.json()
fetchStandard global fetch
URLStandard URL
cryptoWeb Crypto API
HeadersStandard Headers
Avoid the following, which are Node.js-specific and unavailable on edge runtimes:
  • fs, path, os, child_process
  • process.env (use env parameter instead)
  • CommonJS require()
  • Synchronous file I/O

Development vs. production behavior

Development (nue serve)

  • Worker loaded from @shared/server/index.js
  • Hot-reloaded on file changes
  • Simulated Cloudflare headers injected
  • Data models read from @shared/server/data/*.json
  • Static files served from source tree

Preview (nue preview)

  • Worker loaded from @shared/server/index.js
  • No hot reload (restart to pick up changes)
  • Same simulated headers as development
  • Static files served from .dist/
  • Mirrors production behavior closely
In production, deploy your @shared/server/index.js to your edge provider separately from the static .dist/ output. Point your CDN or edge worker at the same routes you registered locally.

When to use server routes vs. static generation

Use static generation (the default) when:
  • Content does not change per-request (blog posts, docs, marketing pages)
  • No authentication or user state is required
  • Maximum CDN cacheability is a priority
Use server routes when:
  • You need to read or write to a database or external API
  • Responses depend on the authenticated user or request headers
  • You are handling form submissions, webhooks, or real-time data
For most Nue sites, the majority of pages are static and only a handful of /api/ routes are dynamic. This hybrid approach gives you the performance benefits of a static site alongside the flexibility of a backend where it matters.

Build docs developers (and LLMs) love